diff --git a/CHANGELOG.md b/CHANGELOG.md index f9d96793d7..a98a6feca0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,12 +19,19 @@ should change the heading of the (upcoming) version to include a major version b ## @rjsf/utils - clear errors on formData change when liveOmit=true when "additionalProperties: false" [issue 1507](https://github.com/rjsf-team/react-jsonschema-form/issues/1507) (https://github.com/rjsf-team/react-jsonschema-form/pull/2631) +## @rjsf/validator-ajv6 +- A **BREAKING CHANGE** to `toErrorList()` was made so that it takes `fieldPath: string[]` rather than `fieldName='root'` as part of the fix (https://github.com/rjsf-team/react-jsonschema-form/issues/1596) + - The returned `errors` also now adds `property` from the `fieldPath` along with the proper path from the `property` to the `stack` message, making it consistent with the AJV errors + - In addition, the extra information provided by AJV is no longer stripped from the `errors` when merged with custom validation, fixing (https://github.com/rjsf-team/react-jsonschema-form/issues/1596). + ## @rjsf/core - **BREAKING CHANGE** Fix overriding core submit button className (https://github.com/rjsf-team/react-jsonschema-form/issues/2979) - Fix `ui:field` with anyOf or oneOf no longer rendered twice (#2890) - **BREAKING CHANGE** Fixed `anyOf` and `oneOf` getting incorrect, potentially duplicate ids when combined with array (https://github.com/rjsf-team/react-jsonschema-form/issues/2197) - `formContext` is now passed properly to `SchemaField`, fixes (https://github.com/rjsf-team/react-jsonschema-form/issues/2394, https://github.com/rjsf-team/react-jsonschema-form/issues/2274) - Added `ui:duplicateKeySuffixSeparator` to customize how duplicate object keys are renamed when using `additionalProperties`. +- The `extraErrors` are now appended onto the end of the schema validation-based `errors` information that is returned via the `onErrors()` callback when submit fails, rather than being merged into the hierarchy. + - In addition, the extra information provided by AJV is no longer stripped from the `errors` during the merge process, fixing (https://github.com/rjsf-team/react-jsonschema-form/issues/1596). ## @rjsf/antd - Fix esm build to use `@rollup/plugin-replace` to replace `antd/lib` and `rc-picker/lib` with `antd/es` and `rc-picker/es` respectively, fixing (https://github.com/rjsf-team/react-jsonschema-form/issues/2962) diff --git a/docs/5.x upgrade guide.md b/docs/5.x upgrade guide.md index b5361eee9d..8b0777ebcf 100644 --- a/docs/5.x upgrade guide.md +++ b/docs/5.x upgrade guide.md @@ -23,7 +23,8 @@ Unfortunately, there is required work pending to properly support React 18, so u ### New packages There are three new packages added in RJSF version 5: -- `@rjsf/utils`: All of the [utility functions](https://react-jsonschema-form.readthedocs.io/en/stable/api-reference/utiltity-functions) previously imported from `@rjsf/core/utils` as well as the Typescript types for RJSV version 5. +- `@rjsf/utils`: All of the [utility functions](https://react-jsonschema-form.readthedocs.io/en/stable/api-reference/utiltity-functions) previously imported from `@rjsf/core/utils` as well as the Typescript types for RJSF version 5. + - The following new utility functions were added: `createSchemaUtils()`, `getInputProps()`, `mergeValidationData()` and `processSelectValue()` - `@rjsf/validator-ajv6`: The [ajv](https://github.com/ajv-validator/ajv)-v6-based validator refactored out of `@rjsf/core@4.x`, that implements the `ValidatorType` interface defined in `@rjsf/utils`. - `@rjsf/mui`: Previously `@rjsf/material-ui/v5`, now provided as its own theme. diff --git a/docs/api-reference/utility-functions.md b/docs/api-reference/utility-functions.md index 35efc1e1ec..cb57851a11 100644 --- a/docs/api-reference/utility-functions.md +++ b/docs/api-reference/utility-functions.md @@ -385,7 +385,7 @@ Converts a UTC date string into a local Date format Returns the superset of `formData` that includes the given set updated to include any missing fields that have computed to have defaults provided in the `schema`. #### Parameters -- validator: ValidatorType - An implementation of the `ValidatorType` interface that will be used when necessary +- validator: ValidatorType - An implementation of the `ValidatorType` interface that will be used when necessary - theSchema: RJSFSchema - The schema for which the default state is desired - [formData]: T - The current formData, if any, onto which to provide any missing defaults - [rootSchema]: RJSFSchema - The root schema, used to primarily to look up `$ref`s @@ -398,7 +398,7 @@ Returns the superset of `formData` that includes the given set updated to includ Determines whether the combination of `schema` and `uiSchema` properties indicates that the label for the `schema` should be displayed in a UI. #### Parameters -- validator: ValidatorType - An implementation of the `ValidatorType` interface that will be used when necessary +- validator: ValidatorType - An implementation of the `ValidatorType` interface that will be used when necessary - schema: RJSFSchema - The schema for which the display label flag is desired - [uiSchema={}]: UiSchema - The UI schema from which to derive potentially displayable information - [rootSchema]: RJSFSchema - The root schema, used to primarily to look up `$ref`s @@ -410,7 +410,7 @@ Determines whether the combination of `schema` and `uiSchema` properties indicat Given the `formData` and list of `options`, attempts to find the index of the option that best matches the data. #### Parameters -- validator: ValidatorType - An implementation of the `ValidatorType` interface that will be used when necessary +- validator: ValidatorType - An implementation of the `ValidatorType` interface that will be used when necessary - formData: T | undefined - The current formData, if any, used to figure out a match - options: RJSFSchema[] - The list of options to find a matching options from - rootSchema: RJSFSchema - The root schema, used to primarily to look up `$ref`s @@ -422,7 +422,7 @@ Given the `formData` and list of `options`, attempts to find the index of the op Checks to see if the `schema` and `uiSchema` combination represents an array of files #### Parameters -- validator: ValidatorType - An implementation of the `ValidatorType` interface that will be used when necessary +- validator: ValidatorType - An implementation of the `ValidatorType` interface that will be used when necessary - schema: RJSFSchema - The schema for which check for array of files flag is desired - [uiSchema={}]: UiSchema - The UI schema from which to check the widget - [rootSchema]: RJSFSchema - The root schema, used to primarily to look up `$ref`s @@ -434,7 +434,7 @@ Checks to see if the `schema` and `uiSchema` combination represents an array of Checks to see if the `schema` combination represents a multi-select #### Parameters -- validator: ValidatorType - An implementation of the `ValidatorType` interface that will be used when necessary +- validator: ValidatorType - An implementation of the `ValidatorType` interface that will be used when necessary - schema: RJSFSchema - The schema for which check for a multi-select flag is desired - [rootSchema]: RJSFSchema - The root schema, used to primarily to look up `$ref`s @@ -445,20 +445,32 @@ Checks to see if the `schema` combination represents a multi-select Checks to see if the `schema` combination represents a select #### Parameters -- validator: ValidatorType - An implementation of the `ValidatorType` interface that will be used when necessary +- validator: ValidatorType - An implementation of the `ValidatorType` interface that will be used when necessary - theSchema: RJSFSchema - The schema for which check for a select flag is desired - [rootSchema]: RJSFSchema - The root schema, used to primarily to look up `$ref`s #### Returns - boolean: True if schema contains a select, otherwise false +### mergeValidationData() +Merges the errors in `additionalErrorSchema` into the existing `validationData` by combining the hierarchies in the two `ErrorSchema`s and then appending the error list from the `additionalErrorSchema` obtained by calling `validator.toErrorList()` onto the `errors` in the `validationData`. +If no `additionalErrorSchema` is passed, then `validationData` is returned. + +#### Parameters +- validator: ValidatorType - An implementation of the `ValidatorType` interface that will be used to convert an ErrorSchema to a list of errors +- validationData: ValidationData - The current `ValidationData` into which to merge the additional errors +- [additionalErrorSchema]: ErrorSchema - The additional set of errors in an `ErrorSchema` + +#### Returns +- ValidationData: The `validationData` with the additional errors from `additionalErrorSchema` merged into it, if provided. + ### retrieveSchema() Retrieves an expanded schema that has had all of its conditions, additional properties, references and dependencies resolved and merged into the `schema` given a `validator`, `rootSchema` and `rawFormData` that is used to do the potentially recursive resolution. #### Parameters -- validator: ValidatorType - An implementation of the `ValidatorType` interface that will be forwarded to all the APIs +- validator: ValidatorType - An implementation of the `ValidatorType` interface that will be forwarded to all the APIs - schema: RJSFSchema - The schema for which retrieving a schema is desired - [rootSchema={}]: RJSFSchema - The root schema that will be forwarded to all the APIs - [rawFormData]: T - The current formData, if any, to assist retrieving a schema @@ -470,7 +482,7 @@ potentially recursive resolution. Generates an `IdSchema` object for the `schema`, recursively #### Parameters -- validator: ValidatorType - An implementation of the `ValidatorType` interface that will be used when necessary +- validator: ValidatorType - An implementation of the `ValidatorType` interface that will be used when necessary - schema: RJSFSchema - The schema for which the `IdSchema` is desired - [id]: string | null - The base id for the schema - [rootSchema]: RJSFSchema - The root schema, used to primarily to look up `$ref`s @@ -485,7 +497,7 @@ Generates an `IdSchema` object for the `schema`, recursively Generates an `PathSchema` object for the `schema`, recursively #### Parameters -- validator: ValidatorType - An implementation of the `ValidatorType` interface that will be used when necessary +- validator: ValidatorType - An implementation of the `ValidatorType` interface that will be used when necessary - schema: RJSFSchema - The schema for which the `PathSchema` is desired - [name='']: string - The base name for the schema - [rootSchema]: RJSFSchema - The root schema, used to primarily to look up `$ref`s @@ -501,7 +513,7 @@ Creates a `SchemaUtilsType` interface that is based around the given `validator` The resulting interface implementation will forward the `validator` and `rootSchema` to all the wrapped APIs. #### Parameters -- validator: ValidatorType - an implementation of the `ValidatorType` interface that will be forwarded to all the APIs +- validator: ValidatorType - an implementation of the `ValidatorType` interface that will be forwarded to all the APIs - rootSchema: RJSFSchema - The root schema that will be forwarded to all the APIs #### Returns diff --git a/docs/usage/validation.md b/docs/usage/validation.md index fde8de512e..0d18036119 100644 --- a/docs/usage/validation.md +++ b/docs/usage/validation.md @@ -123,11 +123,11 @@ render(( Each element in the `errors` list passed to `transformErrors` is a `RJSFValidationError` interface (in `@rjsf/utils`) and has the following properties: -- `name`: name of the error, for example, "required" or "minLength" -- `message`: message, for example, "is a required property" or "should NOT be shorter than 3 characters" -- `params`: an object with the error params returned by ajv ([see doc](https://github.com/ajv-validator/ajv/tree/6a671057ea6aae690b5967ee26a0ddf8452c6297#error-parameters) for more info). -- `property`: a string in Javascript property accessor notation to the data path of the field with the error. For example, `.name` or `['first-name']`. -- `schemaPath`: JSON pointer to the schema of the keyword that failed validation. For example, `#/fields/firstName/required`. (Note: this may sometimes be wrong due to a [bug in ajv](https://github.com/ajv-validator/ajv/issues/512)). +- `name`: optional name of the error, for example, "required" or "minLength" +- `message`: optional message, for example, "is a required property" or "should NOT be shorter than 3 characters" +- `params`: optional object with the error params returned by ajv ([see doc](https://github.com/ajv-validator/ajv/tree/6a671057ea6aae690b5967ee26a0ddf8452c6297#error-parameters) for more info). +- `property`: optional string in Javascript property accessor notation to the data path of the field with the error. For example, `.name` or `.first-name`. +- `schemaPath`: optional JSON pointer to the schema of the keyword that failed validation. For example, `#/fields/firstName/required`. (Note: this may sometimes be wrong due to a [bug in ajv](https://github.com/ajv-validator/ajv/issues/512)). - `stack`: full error name, for example ".name is a required property". ## Error List Display @@ -135,7 +135,7 @@ Each element in the `errors` list passed to `transformErrors` is a `RJSFValidati To take control over how the form errors are displayed, you can define an *error list template* for your form. This list is the form global error list that appears at the top of your forms. -An error list template is basically a React stateless component being passed errors as props so you can render them as you like: +An error list template is basically a React stateless component being passed errors as props, so you can render them as you like: ```tsx import validator from "@rjsf/validator-ajv6"; @@ -167,7 +167,7 @@ render(( showErrorList={true} formData={""} liveValidate - ErrorList={ErrorListTemplate} /> + templates: {{ ErrorListTemplate }} /> ), document.getElementById("app")); ``` @@ -181,7 +181,6 @@ The following props are passed to `ErrorList` as defined by the `ErrorListProps` - `uiSchema`: The uiSchema that was passed to `Form`. - `formContext`: The `formContext` object that you passed to `Form`. - ## The case of empty strings When a text input is empty, the field in form data is set to `undefined`. String fields that use `enum` and a `select` widget will have an empty option at the top of the options list that when selected will result in the field being `undefined`. diff --git a/packages/core/src/components/Form.tsx b/packages/core/src/components/Form.tsx index 1d163f237a..a0592e84a6 100644 --- a/packages/core/src/components/Form.tsx +++ b/packages/core/src/components/Form.tsx @@ -334,12 +334,12 @@ export default class Form extends Component< errorSchema = currentErrors.errorSchema; } if (props.extraErrors) { - errorSchema = mergeObjects( - errorSchema, - props.extraErrors, - true - ) as ErrorSchema; - errors = schemaUtils.getValidator().toErrorList(errorSchema); + const merged = schemaUtils.mergeValidationData( + { errorSchema, errors }, + props.extraErrors + ); + errorSchema = merged.errorSchema; + errors = merged.errors; } const idSchema = schemaUtils.toIdSchema( retrievedSchema, @@ -536,12 +536,12 @@ export default class Form extends Component< const schemaValidationErrors = errors; const schemaValidationErrorSchema = errorSchema; if (extraErrors) { - errorSchema = mergeObjects( - errorSchema, - extraErrors, - true - ) as ErrorSchema; - errors = schemaUtils.getValidator().toErrorList(errorSchema); + const merged = schemaUtils.mergeValidationData( + schemaValidation, + extraErrors + ); + errorSchema = merged.errorSchema; + errors = merged.errors; } state = { formData: newFormData, @@ -633,12 +633,12 @@ export default class Form extends Component< const schemaValidationErrorSchema = errorSchema; if (errors.length > 0) { if (extraErrors) { - errorSchema = mergeObjects( - errorSchema, - extraErrors, - true - ) as ErrorSchema; - errors = schemaUtils.getValidator().toErrorList(errorSchema); + const merged = schemaUtils.mergeValidationData( + schemaValidation, + extraErrors + ); + errorSchema = merged.errorSchema; + errors = merged.errors; } this.setState( { diff --git a/packages/core/test/validate_test.js b/packages/core/test/validate_test.js index d6da8e7bb8..c854f87edf 100644 --- a/packages/core/test/validate_test.js +++ b/packages/core/test/validate_test.js @@ -133,7 +133,7 @@ describe("Validation", () => { submitForm(node); sinon.assert.calledWithMatch(onError.lastCall, [ - { stack: "root: Invalid" }, + { property: ".", stack: ".: Invalid" }, ]); }); @@ -160,7 +160,7 @@ describe("Validation", () => { sinon.assert.calledWithMatch(onChange.lastCall, { errorSchema: { __errors: ["Invalid"] }, - errors: [{ stack: "root: Invalid" }], + errors: [{ property: ".", stack: ".: Invalid" }], formData: "1234", }); }); @@ -242,8 +242,15 @@ describe("Validation", () => { }); submitForm(node); sinon.assert.calledWithMatch(onError.lastCall, [ - { stack: "pass2: should NOT be shorter than 3 characters" }, - { stack: "pass2: Passwords don't match" }, + { + message: "should NOT be shorter than 3 characters", + name: "minLength", + params: { limit: 3 }, + property: ".pass2", + schemaPath: "#/properties/pass2/minLength", + stack: ".pass2 should NOT be shorter than 3 characters", + }, + { property: ".pass2", stack: ".pass2: Passwords don't match" }, ]); }); @@ -281,7 +288,7 @@ describe("Validation", () => { submitForm(node); sinon.assert.calledWithMatch(onError.lastCall, [ - { stack: "pass2: Passwords don't match" }, + { property: ".0.pass2", stack: ".0.pass2: Passwords don't match" }, ]); }); @@ -309,7 +316,7 @@ describe("Validation", () => { }); submitForm(node); sinon.assert.calledWithMatch(onError.lastCall, [ - { stack: "root: Forbidden value: bbb" }, + { property: ".", stack: ".: Forbidden value: bbb" }, ]); }); }); diff --git a/packages/utils/src/createSchemaUtils.ts b/packages/utils/src/createSchemaUtils.ts index af779ba495..6e42a31a46 100644 --- a/packages/utils/src/createSchemaUtils.ts +++ b/packages/utils/src/createSchemaUtils.ts @@ -1,10 +1,12 @@ import deepEquals from "./deepEquals"; import { + ErrorSchema, IdSchema, PathSchema, RJSFSchema, SchemaUtilsType, UiSchema, + ValidationData, ValidatorType, } from "./types"; import { @@ -14,6 +16,7 @@ import { isFilesArray, isMultiSelect, isSelect, + mergeValidationData, retrieveSchema, toIdSchema, toPathSchema, @@ -152,6 +155,26 @@ class SchemaUtils implements SchemaUtilsType { return isSelect(this.validator, schema, this.rootSchema); } + /** Merges the errors in `additionalErrorSchema` into the existing `validationData` by combining the hierarchies in + * the two `ErrorSchema`s and then appending the error list from the `additionalErrorSchema` obtained by calling + * `getValidator().toErrorList()` onto the `errors` in the `validationData`. If no `additionalErrorSchema` is passed, + * then `validationData` is returned. + * + * @param validationData - The current `ValidationData` into which to merge the additional errors + * @param [additionalErrorSchema] - The additional set of errors + * @returns - The `validationData` with the additional errors from `additionalErrorSchema` merged into it, if provided. + */ + mergeValidationData( + validationData: ValidationData, + additionalErrorSchema?: ErrorSchema + ): ValidationData { + return mergeValidationData( + this.validator, + validationData, + additionalErrorSchema + ); + } + /** Retrieves an expanded schema that has had all of its conditions, additional properties, references and * dependencies resolved and merged into the `schema` given a `rawFormData` that is used to do the potentially * recursive resolution. diff --git a/packages/utils/src/schema/index.ts b/packages/utils/src/schema/index.ts index aa08f6d084..479018aafc 100644 --- a/packages/utils/src/schema/index.ts +++ b/packages/utils/src/schema/index.ts @@ -4,6 +4,7 @@ import getMatchingOption from "./getMatchingOption"; import isFilesArray from "./isFilesArray"; import isMultiSelect from "./isMultiSelect"; import isSelect from "./isSelect"; +import mergeValidationData from "./mergeValidationData"; import retrieveSchema from "./retrieveSchema"; import toIdSchema from "./toIdSchema"; import toPathSchema from "./toPathSchema"; @@ -15,6 +16,7 @@ export { isFilesArray, isMultiSelect, isSelect, + mergeValidationData, retrieveSchema, toIdSchema, toPathSchema, diff --git a/packages/utils/src/schema/mergeValidationData.ts b/packages/utils/src/schema/mergeValidationData.ts new file mode 100644 index 0000000000..e98b9eeade --- /dev/null +++ b/packages/utils/src/schema/mergeValidationData.ts @@ -0,0 +1,36 @@ +import isEmpty from "lodash/isEmpty"; + +import mergeObjects from "../mergeObjects"; +import { ErrorSchema, ValidationData, ValidatorType } from "../types"; + +/** Merges the errors in `additionalErrorSchema` into the existing `validationData` by combining the hierarchies in the + * two `ErrorSchema`s and then appending the error list from the `additionalErrorSchema` obtained by calling + * `validator.toErrorList()` onto the `errors` in the `validationData`. If no `additionalErrorSchema` is passed, then + * `validationData` is returned. + * + * @param validator - The validator used to convert an ErrorSchema to a list of errors + * @param validationData - The current `ValidationData` into which to merge the additional errors + * @param [additionalErrorSchema] - The additional set of errors in an `ErrorSchema` + * @returns - The `validationData` with the additional errors from `additionalErrorSchema` merged into it, if provided. + */ +export default function mergeValidationData( + validator: ValidatorType, + validationData: ValidationData, + additionalErrorSchema?: ErrorSchema +): ValidationData { + if (!additionalErrorSchema) { + return validationData; + } + const { errors: oldErrors, errorSchema: oldErrorSchema } = validationData; + let errors = validator.toErrorList(additionalErrorSchema); + let errorSchema = additionalErrorSchema; + if (!isEmpty(oldErrorSchema)) { + errorSchema = mergeObjects( + oldErrorSchema, + additionalErrorSchema, + true + ) as ErrorSchema; + errors = [...oldErrors].concat(errors); + } + return { errorSchema, errors }; +} diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index bdb54a7421..c0be064336 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -714,11 +714,11 @@ export interface ValidatorType { /** Converts an `errorSchema` into a list of `RJSFValidationErrors` * * @param errorSchema - The `ErrorSchema` instance to convert - * @param [fieldName='root'] - The current field name, defaults to `root` if not specified + * @param [fieldPath=[]] - The current field path, defaults to [] if not specified */ toErrorList( errorSchema?: ErrorSchema, - fieldName?: string + fieldPath?: string[] ): RJSFValidationError[]; /** Validates data against a schema, returning true if the data is valid, or * false otherwise. If the schema is invalid, then this function will return @@ -804,6 +804,20 @@ export interface SchemaUtilsType { * @returns - True if schema contains a select, otherwise false */ isSelect(schema: RJSFSchema): boolean; + /** Merges the errors in `additionalErrorSchema` into the existing `validationData` by combining the hierarchies in the + * two `ErrorSchema`s and then appending the error list from the `additionalErrorSchema` obtained by calling + * `validator.toErrorList()` onto the `errors` in the `validationData`. If no `additionalErrorSchema` is passed, then + * `validationData` is returned. + * + * @param validator - The validator used to convert an ErrorSchema to a list of errors + * @param validationData - The current `ValidationData` into which to merge the additional errors + * @param [additionalErrorSchema] - The additional set of errors + * @returns - The `validationData` with the additional errors from `additionalErrorSchema` merged into it, if provided. + */ + mergeValidationData( + validationData: ValidationData, + additionalErrorSchema?: ErrorSchema + ): ValidationData; /** Retrieves an expanded schema that has had all of its conditions, additional properties, references and * dependencies resolved and merged into the `schema` given a `rawFormData` that is used to do the potentially * recursive resolution. diff --git a/packages/utils/test/schema.test.ts b/packages/utils/test/schema.test.ts index 09c4f72e0f..6148baf4af 100644 --- a/packages/utils/test/schema.test.ts +++ b/packages/utils/test/schema.test.ts @@ -6,6 +6,7 @@ import { isFilesArrayTest, isMultiSelectTest, isSelectTest, + mergeValidationDataTest, retrieveSchemaTest, toIdSchemaTest, toPathSchemaTest, @@ -19,6 +20,7 @@ getMatchingOptionTest(testValidator); isFilesArrayTest(testValidator); isMultiSelectTest(testValidator); isSelectTest(testValidator); +mergeValidationDataTest(testValidator); retrieveSchemaTest(testValidator); toIdSchemaTest(testValidator); toPathSchemaTest(testValidator); diff --git a/packages/utils/test/schema/index.ts b/packages/utils/test/schema/index.ts index b5705358e4..cb450a2c46 100644 --- a/packages/utils/test/schema/index.ts +++ b/packages/utils/test/schema/index.ts @@ -4,6 +4,7 @@ import getMatchingOptionTest from "./getMatchingOptionTest"; import isFilesArrayTest from "./isFilesArrayTest"; import isMultiSelectTest from "./isMultiSelectTest"; import isSelectTest from "./isSelectTest"; +import mergeValidationDataTest from "./mergeValidationDataTest"; import retrieveSchemaTest from "./retrieveSchemaTest"; import toIdSchemaTest from "./toIdSchemaTest"; import toPathSchemaTest from "./toPathSchemaTest"; @@ -17,6 +18,7 @@ export { isFilesArrayTest, isMultiSelectTest, isSelectTest, + mergeValidationDataTest, retrieveSchemaTest, toIdSchemaTest, toPathSchemaTest, diff --git a/packages/utils/test/schema/mergeValidationDataTest.ts b/packages/utils/test/schema/mergeValidationDataTest.ts new file mode 100644 index 0000000000..89075dc019 --- /dev/null +++ b/packages/utils/test/schema/mergeValidationDataTest.ts @@ -0,0 +1,60 @@ +import { + ERRORS_KEY, + mergeValidationData, + createSchemaUtils, + ErrorSchema, + ValidationData, +} from "../../src"; +import { TestValidatorType } from "./types"; + +export default function mergeValidationDataTest( + testValidator: TestValidatorType +) { + describe("mergeValidationDataTest()", () => { + it("Returns validationData when no additionalErrorSchema is passed", () => { + const validationData: ValidationData = { + errorSchema: {}, + errors: [], + }; + expect(mergeValidationData(testValidator, validationData)).toBe( + validationData + ); + }); + it("Returns only additionalErrorSchema when additionalErrorSchema is passed and no validationData", () => { + const validationData: ValidationData = { + errorSchema: {}, + errors: [], + }; + const errors = ["custom errors"]; + const customErrors = [{ property: ".", stack: `.: ${errors[0]}` }]; + testValidator.setReturnValues({ errorList: [customErrors] }); + const errorSchema: ErrorSchema = { [ERRORS_KEY]: errors } as ErrorSchema; + const expected = { + errorSchema, + errors: customErrors, + }; + expect( + mergeValidationData(testValidator, validationData, errorSchema) + ).toEqual(expected); + }); + it("Returns merged data when additionalErrorSchema is passed", () => { + const schemaUtils = createSchemaUtils(testValidator, {}); + const oldError = "ajv error"; + const validationData: ValidationData = { + errorSchema: { [ERRORS_KEY]: [oldError] } as ErrorSchema, + errors: [{ stack: oldError, name: "foo", schemaPath: ".foo" }], + }; + const errors = ["custom errors"]; + const customErrors = [{ property: ".", stack: `.: ${errors[0]}` }]; + testValidator.setReturnValues({ errorList: [customErrors] }); + const errorSchema: ErrorSchema = { [ERRORS_KEY]: errors } as ErrorSchema; + const expected = { + errorSchema: { [ERRORS_KEY]: [oldError, ...errors] }, + errors: [...validationData.errors, ...customErrors], + }; + expect( + schemaUtils.mergeValidationData(validationData, errorSchema) + ).toEqual(expected); + }); + }); +} diff --git a/packages/utils/test/schema/types.ts b/packages/utils/test/schema/types.ts index a06dd202d6..a0c8c3d71c 100644 --- a/packages/utils/test/schema/types.ts +++ b/packages/utils/test/schema/types.ts @@ -1,8 +1,9 @@ -import { ValidationData, ValidatorType } from "../../src"; +import { RJSFValidationError, ValidationData, ValidatorType } from "../../src"; export interface TestValidatorParams { isValid?: boolean[]; data?: ValidationData[]; + errorList?: RJSFValidationError[][]; } export interface TestValidatorType extends ValidatorType { diff --git a/packages/utils/test/testUtils/getTestValidator.ts b/packages/utils/test/testUtils/getTestValidator.ts index 465918b94c..18b6299c2b 100644 --- a/packages/utils/test/testUtils/getTestValidator.ts +++ b/packages/utils/test/testUtils/getTestValidator.ts @@ -1,17 +1,20 @@ -import { ValidationData } from "../../src"; +import { RJSFValidationError, ValidationData } from "../../src"; import { TestValidatorParams, TestValidatorType } from "../schema/types"; export default function getTestValidator({ isValid = [], data = [], + errorList = [], }: TestValidatorParams): TestValidatorType { const testValidator: { _data: ValidationData[]; _isValid: boolean[]; + _errorList: RJSFValidationError[][]; validator: TestValidatorType; } = { _data: data, _isValid: isValid, + _errorList: errorList, validator: { validateFormData: jest.fn().mockImplementation(() => { if ( @@ -32,14 +35,26 @@ export default function getTestValidator({ } return true; }), - toErrorList: jest.fn().mockImplementation(() => []), - setReturnValues({ isValid, data }: TestValidatorParams) { + toErrorList: jest.fn().mockImplementation(() => { + // console.warn('isValid', JSON.stringify(args)); + if ( + Array.isArray(testValidator._errorList) && + testValidator._errorList.length > 0 + ) { + return testValidator._errorList.shift(); + } + return []; + }), + setReturnValues({ isValid, data, errorList }: TestValidatorParams) { if (isValid !== undefined) { testValidator._isValid = isValid; } if (data !== undefined) { testValidator._data = data; } + if (errorList !== undefined) { + testValidator._errorList = errorList; + } }, }, }; diff --git a/packages/validator-ajv6/src/validator.ts b/packages/validator-ajv6/src/validator.ts index 65f6c8d09e..799208f2fb 100644 --- a/packages/validator-ajv6/src/validator.ts +++ b/packages/validator-ajv6/src/validator.ts @@ -13,7 +13,7 @@ import { ValidatorType, getDefaultFormState, isObject, - mergeObjects, + mergeValidationData, ERRORS_KEY, REF_KEY, } from "@rjsf/utils"; @@ -107,25 +107,31 @@ export default class AJV6Validator implements ValidatorType { /** Converts an `errorSchema` into a list of `RJSFValidationErrors` * * @param errorSchema - The `ErrorSchema` instance to convert - * @param [fieldName='root'] - The current field name, defaults to `root` if not specified + * @param [fieldPath=[]] - The current field path, defaults to [] if not specified */ - toErrorList(errorSchema?: ErrorSchema, fieldName = "root") { - // XXX: We should transform fieldName as a full field path string. + toErrorList(errorSchema?: ErrorSchema, fieldPath: string[] = []) { if (!errorSchema) { return []; } let errorList: RJSFValidationError[] = []; if (ERRORS_KEY in errorSchema) { errorList = errorList.concat( - errorSchema.__errors!.map((stack: string) => ({ - stack: `${fieldName}: ${stack}`, - })) + errorSchema.__errors!.map((stack: string) => { + const property = "." + fieldPath.join("."); + return { + property, + stack: `${property}: ${stack}`, + }; + }) ); } return Object.keys(errorSchema).reduce((acc, key) => { if (key !== ERRORS_KEY) { acc = acc.concat( - this.toErrorList((errorSchema as GenericObjectType)[key], key) + this.toErrorList((errorSchema as GenericObjectType)[key], [ + ...fieldPath, + key, + ]) ); } return acc; @@ -284,20 +290,11 @@ export default class AJV6Validator implements ValidatorType { this.createErrorHandler(newFormData) ); const userErrorSchema = this.unwrapErrorHandler(errorHandler); - const newErrorSchema: ErrorSchema = mergeObjects( - errorSchema, - userErrorSchema, - true - ) as ErrorSchema; - // XXX: The errors list produced is not fully compliant with the format - // exposed by the jsonschema lib, which contains full field paths and other - // properties. - const newErrors = this.toErrorList(newErrorSchema); - - return { - errors: newErrors, - errorSchema: newErrorSchema as ErrorSchema, - }; + return mergeValidationData( + this, + { errors, errorSchema }, + userErrorSchema + ); } /** Takes a `node` object and transforms any contained `$ref` node variables with a prefix, recursively calling diff --git a/packages/validator-ajv6/test/utilsTests/getTestValidator.ts b/packages/validator-ajv6/test/utilsTests/getTestValidator.ts index f75c118e26..4a3ba70822 100644 --- a/packages/validator-ajv6/test/utilsTests/getTestValidator.ts +++ b/packages/validator-ajv6/test/utilsTests/getTestValidator.ts @@ -30,9 +30,9 @@ export default function getTestValidator( }, toErrorList( errorSchema?: ErrorSchema, - fieldName?: string + fieldPath?: string[] ): RJSFValidationError[] { - return validator.toErrorList(errorSchema, fieldName); + return validator.toErrorList(errorSchema, fieldPath); }, isValid(schema: RJSFSchema, formData: T, rootSchema: RJSFSchema): boolean { return validator.isValid(schema, formData, rootSchema); diff --git a/packages/validator-ajv6/test/utilsTests/schema.test.ts b/packages/validator-ajv6/test/utilsTests/schema.test.ts index fa3318ab50..1b99a1dc68 100644 --- a/packages/validator-ajv6/test/utilsTests/schema.test.ts +++ b/packages/validator-ajv6/test/utilsTests/schema.test.ts @@ -6,6 +6,7 @@ import { isFilesArrayTest, isMultiSelectTest, isSelectTest, + mergeValidationDataTest, retrieveSchemaTest, toIdSchemaTest, toPathSchemaTest, @@ -20,6 +21,7 @@ getMatchingOptionTest(testValidator); isFilesArrayTest(testValidator); isMultiSelectTest(testValidator); isSelectTest(testValidator); +mergeValidationDataTest(testValidator); retrieveSchemaTest(testValidator); toIdSchemaTest(testValidator); toPathSchemaTest(testValidator); diff --git a/packages/validator-ajv6/test/validator.test.ts b/packages/validator-ajv6/test/validator.test.ts index 66eb320e53..6c2dc372dc 100644 --- a/packages/validator-ajv6/test/validator.test.ts +++ b/packages/validator-ajv6/test/validator.test.ts @@ -123,11 +123,11 @@ describe("AJV6Validator", () => { } as ErrorSchema, } as unknown as ErrorSchema; expect(validator.toErrorList(errorSchema)).toEqual([ - { stack: "root: err1" }, - { stack: "root: err2" }, - { stack: "b: err3" }, - { stack: "b: err4" }, - { stack: "c: err5" }, + { property: ".", stack: ".: err1" }, + { property: ".", stack: ".: err2" }, + { property: ".a.b", stack: ".a.b: err3" }, + { property: ".a.b", stack: ".a.b: err4" }, + { property: ".c", stack: ".c: err5" }, ]); }); }); @@ -284,7 +284,7 @@ describe("AJV6Validator", () => { }); it("should return an error list", () => { expect(errors).toHaveLength(1); - expect(errors[0].stack).toEqual("pass2: passwords don`t match."); + expect(errors[0].stack).toEqual(".pass2: passwords don`t match."); }); it("should return an errorSchema", () => { expect(errorSchema.pass2!.__errors).toHaveLength(1); @@ -319,7 +319,7 @@ describe("AJV6Validator", () => { }); it("should return an error list", () => { expect(errors).toHaveLength(1); - expect(errors[0].stack).toEqual("pass2: passwords don`t match."); + expect(errors[0].stack).toEqual(".pass2: passwords don`t match."); }); it("should return an errorSchema", () => { expect(errorSchema.pass2!.__errors).toHaveLength(1);