From eb7a42376bce6b61768e679b01b37925f1de92b0 Mon Sep 17 00:00:00 2001 From: Heath Chiavettone Date: Tue, 9 May 2023 16:34:51 -0700 Subject: [PATCH] fix: Implement precompiled validator support in @rjsf/validator-ajv8 Fixes #3543 by implementing support for precompiled validators - In `@rjsf/validator-ajv8` added support for precompiled validators as follows: - Added a new `compileSchemaValidators()` API function used to generate the precompiled validators for a schema to an output file - Updated the documentation for the `customizeValidator()` API function - Added a new `AJV8PrecompiledValidator` implementation of the `ValidatorType` interface - Refactored a large piece of the raw validation error processing from the `AJV8Validator` into a new `processRawValidationErrors()` function also used by the `AJV8PrecompiledValidator` - Added a new `usePrecompiledValidator()` API function that is similar to `customizeValidator()` but returning a precompiled validator-based `ValidatorType` interface implementation - Added some new types to the `types.ts` file in support of precompiled validators - Updated the main `index.ts` file to export the new types and API functions - Added 100% unit test coverage of the new feature - This included implementing a node function to precompile the `superSchema.json` file found in the `test/harness` directory - Added `ignorePatterns` to the `.eslintrc` file to ignore the precompiled schema files - Updated the `validation.md` documentation for the new precompiled validator functionality - Added a new `validator-ajv8.md` documentation file to the `api-reference` directory and the `sidebar.js` - Updated the `CHANGELOG.md` file accordingly --- CHANGELOG.md | 10 +- README.md | 2 +- .../docs/docs/api-reference/validator-ajv8.md | 57 ++ packages/docs/docs/usage/validation.md | 76 ++ packages/docs/sidebars.js | 1 + .../src/compileSchemaValidators.ts | 35 + .../validator-ajv8/src/customizeValidator.ts | 4 +- packages/validator-ajv8/src/index.ts | 4 +- .../src/precompiledValidator.ts | 166 ++++ .../src/processRawValidationErrors.ts | 139 ++++ packages/validator-ajv8/src/types.ts | 17 +- .../src/usePrecompiledValidator.ts | 23 + packages/validator-ajv8/src/validator.ts | 114 +-- packages/validator-ajv8/test/.eslintrc | 3 + .../test/compileSchemaValidators.test.ts | 98 +++ .../test/createAjvInstance.test.ts | 17 +- .../test/customizeValidator.test.ts | 2 +- .../test/harness/compileTestSchema.js | 30 + .../test/harness/superSchema.js | 732 +++++++++++++++++ .../test/harness/superSchema.json | 76 ++ .../test/harness/superSchemaOptions.js | 745 ++++++++++++++++++ .../validator-ajv8/test/harness/testData.ts | 17 + .../test/precompiledValidator.test.ts | 365 +++++++++ .../test/processRawValidationErrors.test.ts | 19 + .../test/usePrecompiledValidator.test.ts | 48 ++ .../validator-ajv8/test/validator.test.ts | 65 +- packages/validator-ajv8/tsconfig.json | 1 + 27 files changed, 2694 insertions(+), 172 deletions(-) create mode 100644 packages/docs/docs/api-reference/validator-ajv8.md create mode 100644 packages/validator-ajv8/src/compileSchemaValidators.ts create mode 100644 packages/validator-ajv8/src/precompiledValidator.ts create mode 100644 packages/validator-ajv8/src/processRawValidationErrors.ts create mode 100644 packages/validator-ajv8/src/usePrecompiledValidator.ts create mode 100644 packages/validator-ajv8/test/compileSchemaValidators.test.ts create mode 100644 packages/validator-ajv8/test/harness/compileTestSchema.js create mode 100644 packages/validator-ajv8/test/harness/superSchema.js create mode 100644 packages/validator-ajv8/test/harness/superSchema.json create mode 100644 packages/validator-ajv8/test/harness/superSchemaOptions.js create mode 100644 packages/validator-ajv8/test/harness/testData.ts create mode 100644 packages/validator-ajv8/test/precompiledValidator.test.ts create mode 100644 packages/validator-ajv8/test/processRawValidationErrors.test.ts create mode 100644 packages/validator-ajv8/test/usePrecompiledValidator.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a00dcdc2d5..4ce6304484 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,13 +39,19 @@ should change the heading of the (upcoming) version to include a major version b - Updated `getDefaultFormState()` and `toPathSchema()` to use `getDiscriminatorFieldFromSchema()` to provide a discriminator field to `getClosestMatchingOption()` calls. - Refactored the `retrieveSchema()` internal API functions to support implementing an internal `schemaParser()` API for use in precompiling schemas, in support of [#3543](https://github.com/rjsf-team/react-jsonschema-form/issues/3543) - Fixed `toPathSchema()` to handle `properties` in an object along with `anyOf`/`oneOf`, fixing [#3628](https://github.com/rjsf-team/react-jsonschema-form/issues/3628) and [#1628](https://github.com/rjsf-team/react-jsonschema-form/issues/1628) -- Refactored optional parameters for `computeDefaults()` into destructured props object to reduce clutter when only specifying later of the optional argument ([3604](https://github.com/rjsf-team/react-jsonschema-form/pull/3604)) +- Refactored optional parameters for `computeDefaults()` into destructured props object to reduce clutter when only specifying later of the optional argument, fixing [#3602](https://github.com/rjsf-team/react-jsonschema-form/issues/3602) - Fixed `computeDefaults()` to handle `$ref` in an object along with `anyOf`/`oneOf`, fixing [#3633](https://github.com/rjsf-team/react-jsonschema-form/issues/3633) +## @rjsf/validator-ajv8 + +- Added two new APIs `compileSchemaValidators()` and `usePrecompiledValidator()` implemented to support using precompiled validators build with AJV 8, fixing [#3543](https://github.com/rjsf-team/react-jsonschema-form/issues/3543) + ## Dev / docs / playground - Added documentation to `custom-templates` describing how to extend the `BaseInputTemplate` -- Added **minItems behavior for array field** live setting ([3604](https://github.com/rjsf-team/react-jsonschema-form/pull/3604)) +- Added **minItems behavior for array field** live setting, fixing [#3602](https://github.com/rjsf-team/react-jsonschema-form/issues/3602) +- Added documentation to `validation` describing the new precompiled validators feature +- Added new `validator-ajv8.md` documentation to the `api-reference` directory as well as putting it into the `sidebar.js` # 5.6.2 diff --git a/README.md b/README.md index 2886148901..13ce27f8a3 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,11 @@ - [Ant Design](https://github.com/rjsf-team/react-jsonschema-form/tree/main/packages/antd) - [Bootstrap 3](https://github.com/rjsf-team/react-jsonschema-form/tree/main/packages/core) - [Bootstrap 4](https://github.com/rjsf-team/react-jsonschema-form/tree/main/packages/bootstrap-4) +- [Chakra UI](https://github.com/rjsf-team/react-jsonschema-form/tree/main/packages/chakra-ui) - [Fluent UI](https://github.com/rjsf-team/react-jsonschema-form/tree/main/packages/fluent-ui) - [Material UI 4](https://github.com/rjsf-team/react-jsonschema-form/tree/main/packages/material-ui) - [Material UI 5](https://github.com/rjsf-team/react-jsonschema-form/tree/main/packages/mui) - [Semantic UI](https://github.com/rjsf-team/react-jsonschema-form/tree/main/packages/semantic-ui) -- [Chakra UI](https://github.com/rjsf-team/react-jsonschema-form/tree/main/packages/chakra-ui) ## Documentation diff --git a/packages/docs/docs/api-reference/validator-ajv8.md b/packages/docs/docs/api-reference/validator-ajv8.md new file mode 100644 index 0000000000..fd5562539c --- /dev/null +++ b/packages/docs/docs/api-reference/validator-ajv8.md @@ -0,0 +1,57 @@ +# @rjsf/validator-ajv8 APIs + +In RJSF version 5, the original, embedded AJV 6 validator implementation from earlier versions was extracted into its own package, `@rjsf/validator-ajv6`, which was immediately deprecated since AJV 6 is no longer getting maintenance updates. +A new `@rjsf/validator-ajv8` package was added that uses the AJV 8 package, including adding support for using precompiled validators. +Below are the exported API functions that are provided by this package. +See the [Validation documentation](../usage/validation.md) for examples of using these APIs. + +## Types + +There are a few Typescript types that are exported by `@rjsf/validator-ajv8` in support of the APIs. + +These types can be found on GitHub [here](https://github.com/rjsf-team/react-jsonschema-form/blob/main/packages/validator-ajv8/src/types.ts). + +## APIs + +### customizeValidator() + +Creates and returns a customized implementation of the `ValidatorType` with the given customization `options` if provided. +If a `localizer` is provided, it is used to translate the messages generated by the underlying AJV validation. + +#### Parameters + +- [options={}]: CustomValidatorOptionsType - The optional map of `CustomValidatorOptionsType` options that are used to create the `ValidatorType` instance +- [localizer]: Localizer | undefined - If provided, is used to localize a list of Ajv `ErrorObject`s after running the form validation using AJV + +#### Returns + +- ValidatorType: The custom validator implementation resulting from the set of parameters provided + +### compileSchemaValidators<S extends StrictRJSFSchema = RJSFSchema>() + +The function used to compile a schema into an output file in the form that allows it to be used as a precompiled validator. +The main reasons for using a precompiled validator is reducing code size, improving validation speed and, most importantly, avoiding dynamic code compilation when prohibited by a browser's Content Security Policy. +For more information about AJV code compilation see: https://ajv.js.org/standalone.html + +#### Parameters + +- schema: S - The schema to be compiled into a set of precompiled validators functions +- output: string - The name of the file into which the precompiled validator functions will be generated +- [options={}]: CustomValidatorOptionsType - The set of `CustomValidatorOptionsType` information used to alter the AJV validator used for compiling the schema. They are the same options that are passed to the `customizeValidator()` function in order to modify the behavior of the regular AJV-based validator. + +### usePrecompiledValidator() + +Creates and returns a `ValidatorType` interface that is implemented with a precompiled validator. +If a `localizer` is provided, it is used to translate the messages generated by the underlying AJV validation. + +> NOTE: The `validateFns` parameter is an object obtained by importing from a precompiled validation file created via the `compileSchemaValidators()` function. + +#### Parameters + +- validateFns: ValidatorFunctions<T> - The map of the validation functions that are created by the `compileSchemaValidators()` function +- rootSchema: S - The root schema that was used with the `compileSchemaValidators()` function +- [localizer]: Localizer | undefined - If provided, is used to localize a list of Ajv `ErrorObject`s after running the form validation using AJV + +#### Returns + +- ValidatorType<T, S, F>: The precompiled validator implementation resulting from the set of parameters provided diff --git a/packages/docs/docs/usage/validation.md b/packages/docs/docs/usage/validation.md index 15848bcc0a..11aa12d61e 100644 --- a/packages/docs/docs/usage/validation.md +++ b/packages/docs/docs/usage/validation.md @@ -13,6 +13,82 @@ If you depend on having specifically formatted messages, then using this validat It is also possible for you to provide your own implementation if you desire, as long as it fulfills the `ValidatorType` interface specified in `@rjsf/utils`. +## API documentation + +The documentation for the APIs associated with the AJV 8 validator package can be found [here](../api-reference/validator-ajv8.md) + +## Precompiled validators + +In 5.7.0, support for precompiled validators was added to the `@rjsf/validator-ajv8` package. +The main use case for this is to overcome issues with `unsafe-eval` warnings from the browser caused by strict Content Security Policy settings. +See the [Standalone Validation Code](https://ajv.js.org/standalone.html) section of the AJV documentation for more details about precompiled validators. + +Due to how RJSF uses the AJV validator in determining `anyOf/oneOf` selections and how it resolves dependencies, if-then-else and references ($ref) in schemas via the `retrieveSchema()` utility method, RJSF provides its own schema compilation API built on-top-of the one provided by AJV 8. +If you are wanting to use a precompiled validator, two steps are required: + +1. Precompiling the schema into a set of validator functions +2. Providing those precompiled validator functions to a `ValidatorType` implementation in the `Form` + +### Schema precompilation + +The first step in the process is to compile a schema into a set of validator functions that are saved into a commonJS-based Javascript file. +The `@rjsf/validator-ajv8` package exports the `compileSchemaValidators()` function that does this. +It is expected that this function will be used in a manner similar to the following: + +```cjs +const { compileSchemaValidators } = require('@rjsf/validator-ajv8'); +const yourSchema = require('path_to/yourSchema'); // If your schema is a js file + +compileSchemaValidators(yourSchema, 'path_to/yourCompiledSchema.js'); +``` + +If you are currently using the `customizeValidator()` function to provide `additionalMetaSchemas`, `customFormats`, `ajvOptionsOverrides` and/or `ajvFormatOptions` then you can pass those in as the optional third parameter to the `compileSchemaValidators()` function in a manner similar to: + +```cjs +const { compileSchemaValidators } = require('@rjsf/validator-ajv8'); +const yourSchema = require('path_to/yourSchema.json'); // If your schema is a json file + +const options = { + additionalMetaSchemas: [require('ajv/lib/refs/json-schema-draft-06.json')], + customFormats: { 'phone-us': /\(?\d{3}\)?[\s-]?\d{3}[\s-]?\d{4}$/, 'area-code': /\d{3}/ }, + ajvOptionsOverrides: { + $data: true, + verbose: true, + }, + ajvFormatOptions: { + mode: 'fast', + }, +}; + +compileSchemaValidators(yourSchema, 'path_to/yourCompiledSchema.js', options); +``` + +It is highly recommended to create a `compileYourSchema.js` file (or what ever name you want) with code similar to what is shown above and then, using node, run the code as follows: + +``` +node compileYourSchema.js +``` + +> NOTE: You must have your schema provided within a file that can be parsed and turned into the set of precompiled validator functions. + +### Using the precompiled validator + +After you have completed step 1 having generated your precompiled schema functions into the `yourCompiledSchema.js` output file (or whatever you called it), then you need to create a `ValidatorType` implementation from it to use in the `Form`. +The `@rjsf/validator-ajv8` package exports the `usePrecompiledValidator()` function for this. +Here is an example of how to use your precompiled validator with your `Form`: + +```tsx +import { usePrecompiledValidator, ValidatorFunctions } from '@rjsf/validator-ajv8'; +import Form from '@rjsf/core'; // Or whatever theme you use + +import yourSchema from 'path_to/yourSchema'; // This needs to be the same file that was precompiled +import * as precompiledValidator from 'path_to/yourCompiledSchema'; + +const validator = usePrecompiledValidator(precompiledValidator as ValidatorFunctions); + +render(
, document.getElementById('app')); +``` + ## Live validation By default, form data are only validated when the form is submitted or when a new `formData` prop is passed to the `Form` component. diff --git a/packages/docs/sidebars.js b/packages/docs/sidebars.js index 6fbfa9a95a..837ed77327 100644 --- a/packages/docs/sidebars.js +++ b/packages/docs/sidebars.js @@ -79,6 +79,7 @@ const sidebars = { 'api-reference/themes/chakra-ui/uiSchema', 'api-reference/form-props', 'api-reference/utility-functions', + 'api-reference/validator-ajv8' ], }, { diff --git a/packages/validator-ajv8/src/compileSchemaValidators.ts b/packages/validator-ajv8/src/compileSchemaValidators.ts new file mode 100644 index 0000000000..5f499a348e --- /dev/null +++ b/packages/validator-ajv8/src/compileSchemaValidators.ts @@ -0,0 +1,35 @@ +import fs from 'fs'; +import standaloneCode from 'ajv/dist/standalone'; +import { RJSFSchema, StrictRJSFSchema, schemaParser } from '@rjsf/utils'; + +import createAjvInstance from './createAjvInstance'; +import { CustomValidatorOptionsType } from './types'; + +/** The function used to compile a schema into an output file in the form that allows it to be used as a precompiled + * validator. The main reasons for using a precompiled validator is reducing code size, improving validation speed and, + * most importantly, avoiding dynamic code compilation when prohibited by a browser's Content Security Policy. For more + * information about AJV code compilation see: https://ajv.js.org/standalone.html + * + * @param schema - The schema to be compiled into a set of precompiled validators functions + * @param output - The name of the file into which the precompiled validator functions will be generated + * @param [options={}] - The set of `CustomValidatorOptionsType` information used to alter the AJV validator used for + * compiling the schema. They are the same options that are passed to the `customizeValidator()` function in + * order to modify the behavior of the regular AJV-based validator. + */ +export default function compileSchemaValidators( + schema: S, + output: string, + options: CustomValidatorOptionsType = {} +) { + console.log('parsing the schema'); + const schemaMaps = schemaParser(schema); + const schemas = Object.values(schemaMaps); + + const { additionalMetaSchemas, customFormats, ajvOptionsOverrides = {}, ajvFormatOptions, AjvClass } = options; + const compileOptions = { ...ajvOptionsOverrides, code: { source: true, lines: true }, schemas }; + const ajv = createAjvInstance(additionalMetaSchemas, customFormats, compileOptions, ajvFormatOptions, AjvClass); + + const moduleCode = standaloneCode(ajv); + console.log(`writing ${output}`); + fs.writeFileSync(output, moduleCode); +} diff --git a/packages/validator-ajv8/src/customizeValidator.ts b/packages/validator-ajv8/src/customizeValidator.ts index 96ced14732..696111ac4d 100644 --- a/packages/validator-ajv8/src/customizeValidator.ts +++ b/packages/validator-ajv8/src/customizeValidator.ts @@ -4,10 +4,12 @@ import { CustomValidatorOptionsType, Localizer } from './types'; import AJV8Validator from './validator'; /** Creates and returns a customized implementation of the `ValidatorType` with the given customization `options` if - * provided. + * provided. If a `localizer` is provided, it is used to translate the messages generated by the underlying AJV + * validation. * * @param [options={}] - The `CustomValidatorOptionsType` options that are used to create the `ValidatorType` instance * @param [localizer] - If provided, is used to localize a list of Ajv `ErrorObject`s + * @returns - The custom validator implementation resulting from the set of parameters provided */ export default function customizeValidator< T = any, diff --git a/packages/validator-ajv8/src/index.ts b/packages/validator-ajv8/src/index.ts index d5df24a351..a891dd57e5 100644 --- a/packages/validator-ajv8/src/index.ts +++ b/packages/validator-ajv8/src/index.ts @@ -1,6 +1,8 @@ import customizeValidator from './customizeValidator'; +import compileSchemaValidators from './compileSchemaValidators'; +import usePrecompiledValidator from './usePrecompiledValidator'; -export { customizeValidator }; +export { customizeValidator, compileSchemaValidators, usePrecompiledValidator }; export * from './types'; export default customizeValidator(); diff --git a/packages/validator-ajv8/src/precompiledValidator.ts b/packages/validator-ajv8/src/precompiledValidator.ts new file mode 100644 index 0000000000..5a6fd793dd --- /dev/null +++ b/packages/validator-ajv8/src/precompiledValidator.ts @@ -0,0 +1,166 @@ +import { ErrorObject } from 'ajv'; +import get from 'lodash/get'; +import isEqual from 'lodash/isEqual'; +import { + CustomValidator, + ErrorSchema, + ErrorTransformer, + FormContextType, + hashForSchema, + ID_KEY, + RJSFSchema, + StrictRJSFSchema, + toErrorList, + UiSchema, + ValidationData, + ValidatorType, +} from '@rjsf/utils'; + +import { CompiledValidateFunction, Localizer, ValidatorFunctions } from './types'; +import processRawValidationErrors, { RawValidationErrorsType } from './processRawValidationErrors'; + +/** `ValidatorType` implementation that uses an AJV 8 precompiled validator as created by the + * `compileSchemaValidators()` function provided by the `@rjsf/validator-ajv8` library. + */ +export default class AJV8PrecompiledValidator< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any +> implements ValidatorType +{ + /** The root schema object used to construct this validator + * + * @private + */ + readonly rootSchema: S; + + /** The `ValidatorFunctions` map used to construct this validator + * + * @private + */ + readonly validateFns: ValidatorFunctions; + + /** The main validator function associated with the base schema in the `precompiledValidator` + * + * @private + */ + readonly mainValidator: CompiledValidateFunction; + + /** The Localizer function to use for localizing Ajv errors + * + * @private + */ + readonly localizer?: Localizer; + + /** Constructs an `AJV8PrecompiledValidator` instance using the `validateFns` and `rootSchema` + * + * @param validateFns - The map of the validation functions that are generated by the `schemaCompile()` function + * @param rootSchema - The root schema that was used with the `compileSchema()` function + * @param [localizer] - If provided, is used to localize a list of Ajv `ErrorObject`s + * @throws - Error when the base schema of the precompiled validator does not have a matching validator function + */ + constructor(validateFns: ValidatorFunctions, rootSchema: S, localizer?: Localizer) { + this.rootSchema = rootSchema; + this.validateFns = validateFns; + this.localizer = localizer; + this.mainValidator = this.getValidator(rootSchema); + } + + /** Returns the precompiled validator associated with the given `schema` from the map of precompiled validator + * functions. + * + * @param schema - The schema for which a precompiled validator function is desired + * @returns - The precompiled validator function associated with this schema + */ + getValidator(schema: S) { + const key = get(schema, ID_KEY, hashForSchema(schema)); + const validator = this.validateFns[key]; + if (!validator) { + throw new Error(`No precompiled validator function was found for the given schema for "${key}"`); + } + return validator; + } + + /** Converts an `errorSchema` into a list of `RJSFValidationErrors` + * + * @param errorSchema - The `ErrorSchema` instance to convert + * @param [fieldPath=[]] - The current field path, defaults to [] if not specified + * @deprecated - Use the `toErrorList()` function provided by `@rjsf/utils` instead. This function will be removed in + * the next major release. + */ + toErrorList(errorSchema?: ErrorSchema, fieldPath: string[] = []) { + return toErrorList(errorSchema, fieldPath); + } + + /** Runs the pure validation of the `schema` and `formData` without any of the RJSF functionality. Provided for use + * by the playground. Returns the `errors` from the validation + * + * @param schema - The schema against which to validate the form data * @param schema + * @param formData - The form data to validate + * @throws - Error when the schema provided does not match the base schema of the precompiled validator + */ + rawValidation(schema: S, formData?: T): RawValidationErrorsType { + const validationError: Error | undefined = undefined; + if (!isEqual(schema, this.rootSchema)) { + throw new Error( + 'The schema associated with the precompiled schema differs from the schema provided for validation' + ); + } + this.mainValidator(formData); + + if (typeof this.localizer === 'function') { + this.localizer(this.mainValidator.errors); + } + const errors = this.mainValidator.errors || undefined; + + // Clear errors to prevent persistent errors, see #1104 + this.mainValidator.errors = null; + + return { + errors: errors as unknown as Result[], + validationError, + }; + } + + /** This function processes the `formData` with an optional user contributed `customValidate` function, which receives + * the form data and a `errorHandler` function that will be used to add custom validation errors for each field. Also + * supports a `transformErrors` function that will take the raw AJV validation errors, prior to custom validation and + * transform them in what ever way it chooses. + * + * @param formData - The form data to validate + * @param schema - The schema against which to validate the form data + * @param [customValidate] - An optional function that is used to perform custom validation + * @param [transformErrors] - An optional function that is used to transform errors after AJV validation + * @param [uiSchema] - An optional uiSchema that is passed to `transformErrors` and `customValidate` + */ + validateFormData( + formData: T | undefined, + schema: S, + customValidate?: CustomValidator, + transformErrors?: ErrorTransformer, + uiSchema?: UiSchema + ): ValidationData { + const rawErrors = this.rawValidation(schema, formData); + return processRawValidationErrors(this, rawErrors, formData, schema, customValidate, transformErrors, uiSchema); + } + + /** 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 false. + * + * @param schema - The schema against which to validate the form data + * @param formData - The form data to validate + * @param rootSchema - The root schema used to provide $ref resolutions + * @returns - true if the formData validates against the schema, false otherwise + * @throws - Error when the schema provided does not match the base schema of the precompiled validator OR if there + * isn't a precompiled validator function associated with the schema + */ + isValid(schema: S, formData: T | undefined, rootSchema: S) { + if (!isEqual(rootSchema, this.rootSchema)) { + throw new Error( + 'The schema associated with the precompiled validator differs from the rootSchema provided for validation' + ); + } + const validator = this.getValidator(schema); + return validator(formData); + } +} diff --git a/packages/validator-ajv8/src/processRawValidationErrors.ts b/packages/validator-ajv8/src/processRawValidationErrors.ts new file mode 100644 index 0000000000..302a498adf --- /dev/null +++ b/packages/validator-ajv8/src/processRawValidationErrors.ts @@ -0,0 +1,139 @@ +import { ErrorObject } from 'ajv'; +import get from 'lodash/get'; +import { + createErrorHandler, + CustomValidator, + ErrorTransformer, + FormContextType, + getDefaultFormState, + getUiOptions, + PROPERTIES_KEY, + RJSFSchema, + RJSFValidationError, + StrictRJSFSchema, + toErrorSchema, + UiSchema, + unwrapErrorHandler, + validationDataMerge, + ValidatorType, +} from '@rjsf/utils'; + +export type RawValidationErrorsType = { errors?: Result[]; validationError?: Error }; + +/** Transforming the error output from ajv to format used by @rjsf/utils. + * At some point, components should be updated to support ajv. + * + * @param errors - The list of AJV errors to convert to `RJSFValidationErrors` + * @param [uiSchema] - An optional uiSchema that is passed to `transformErrors` and `customValidate` + */ +export function transformRJSFValidationErrors< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any +>(errors: ErrorObject[] = [], uiSchema?: UiSchema): RJSFValidationError[] { + return errors.map((e: ErrorObject) => { + const { instancePath, keyword, params, schemaPath, parentSchema, ...rest } = e; + let { message = '' } = rest; + let property = instancePath.replace(/\//g, '.'); + let stack = `${property} ${message}`.trim(); + + if ('missingProperty' in params) { + property = property ? `${property}.${params.missingProperty}` : params.missingProperty; + const currentProperty: string = params.missingProperty; + const uiSchemaTitle = getUiOptions(get(uiSchema, `${property.replace(/^\./, '')}`)).title; + + if (uiSchemaTitle) { + message = message.replace(currentProperty, uiSchemaTitle); + } else { + const parentSchemaTitle = get(parentSchema, [PROPERTIES_KEY, currentProperty, 'title']); + + if (parentSchemaTitle) { + message = message.replace(currentProperty, parentSchemaTitle); + } + } + + stack = message; + } else { + const uiSchemaTitle = getUiOptions(get(uiSchema, `${property.replace(/^\./, '')}`)).title; + + if (uiSchemaTitle) { + stack = `'${uiSchemaTitle}' ${message}`.trim(); + } else { + const parentSchemaTitle = parentSchema?.title; + + if (parentSchemaTitle) { + stack = `'${parentSchemaTitle}' ${message}`.trim(); + } + } + } + + // put data in expected format + return { + name: keyword, + property, + message, + params, // specific to ajv + stack, + schemaPath, + }; + }); +} + +/** This function processes the `formData` with an optional user contributed `customValidate` function, which receives + * the form data and a `errorHandler` function that will be used to add custom validation errors for each field. Also + * supports a `transformErrors` function that will take the raw AJV validation errors, prior to custom validation and + * transform them in what ever way it chooses. + * + * @param validator - The `ValidatorType` implementation used for the `getDefaultFormState()` call + * @param rawErrors - The list of raw `ErrorObject`s to process + * @param formData - The form data to validate + * @param schema - The schema against which to validate the form data + * @param [customValidate] - An optional function that is used to perform custom validation + * @param [transformErrors] - An optional function that is used to transform errors after AJV validation + * @param [uiSchema] - An optional uiSchema that is passed to `transformErrors` and `customValidate` + */ +export default function processRawValidationErrors< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any +>( + validator: ValidatorType, + rawErrors: RawValidationErrorsType, + formData: T | undefined, + schema: S, + customValidate?: CustomValidator, + transformErrors?: ErrorTransformer, + uiSchema?: UiSchema +) { + const { validationError: invalidSchemaError } = rawErrors; + let errors = transformRJSFValidationErrors(rawErrors.errors, uiSchema); + + if (invalidSchemaError) { + errors = [...errors, { stack: invalidSchemaError!.message }]; + } + if (typeof transformErrors === 'function') { + errors = transformErrors(errors, uiSchema); + } + + let errorSchema = toErrorSchema(errors); + + if (invalidSchemaError) { + errorSchema = { + ...errorSchema, + $schema: { + __errors: [invalidSchemaError!.message], + }, + }; + } + + if (typeof customValidate !== 'function') { + return { errors, errorSchema }; + } + + // Include form data with undefined values, which is required for custom validation. + const newFormData = getDefaultFormState(validator, schema, formData, schema, true) as T; + + const errorHandler = customValidate(newFormData, createErrorHandler(newFormData), uiSchema); + const userErrorSchema = unwrapErrorHandler(errorHandler); + return validationDataMerge({ errors, errorSchema }, userErrorSchema); +} diff --git a/packages/validator-ajv8/src/types.ts b/packages/validator-ajv8/src/types.ts index f4b7626d84..7900fe4ac1 100644 --- a/packages/validator-ajv8/src/types.ts +++ b/packages/validator-ajv8/src/types.ts @@ -1,5 +1,6 @@ -import Ajv, { Options, ErrorObject } from 'ajv'; +import Ajv, { Options, ErrorObject, ValidateFunction } from 'ajv'; import { FormatsPluginOptions } from 'ajv-formats'; +import { DataValidationCxt } from 'ajv/lib/types'; /** The type describing how to customize the AJV6 validator */ @@ -21,3 +22,17 @@ export interface CustomValidatorOptionsType { /** The type describing a function that takes a list of Ajv `ErrorObject`s and localizes them */ export type Localizer = (errors?: null | ErrorObject[]) => void; + +/** Extend ValidateFunction to Omit its two required properties, `schema` and `schemaEnv` that are not produced by the + * AJV schema standalone compilation code + */ +export interface CompiledValidateFunction extends Omit, 'schema' | 'schemaEnv'> { + /** This is literally copied from the `ValidateFunction` type definition from which it extends because it seems to get + * lost as part of the Omit<>. + */ + (this: Ajv | any, data: any, dataCxt?: DataValidationCxt): boolean; +} + +/** The definition of precompiled validator functions + */ +export type ValidatorFunctions = { [key: string]: CompiledValidateFunction }; diff --git a/packages/validator-ajv8/src/usePrecompiledValidator.ts b/packages/validator-ajv8/src/usePrecompiledValidator.ts new file mode 100644 index 0000000000..4abd685976 --- /dev/null +++ b/packages/validator-ajv8/src/usePrecompiledValidator.ts @@ -0,0 +1,23 @@ +import { FormContextType, RJSFSchema, StrictRJSFSchema, ValidatorType } from '@rjsf/utils'; + +import { Localizer, ValidatorFunctions } from './types'; +import AJV8PrecompiledValidator from './precompiledValidator'; + +/** Creates and returns a `ValidatorType` interface that is implemented with a precompiled validator. If a `localizer` + * is provided, it is used to translate the messages generated by the underlying AJV validation. + * + * NOTE: The `validateFns` parameter is an object obtained by importing from a precompiled validation file created via + * the `compileSchemaValidators()` function. + * + * @param validateFns - The map of the validation functions that are created by the `compileSchemaValidators()` function + * @param rootSchema - The root schema that was used with the `compileSchemaValidators()` function + * @param [localizer] - If provided, is used to localize a list of Ajv `ErrorObject`s + * @returns - The precompiled validator implementation resulting from the set of parameters provided + */ +export default function usePrecompiledValidator< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any +>(validateFns: ValidatorFunctions, rootSchema: S, localizer?: Localizer): ValidatorType { + return new AJV8PrecompiledValidator(validateFns, rootSchema, localizer); +} diff --git a/packages/validator-ajv8/src/validator.ts b/packages/validator-ajv8/src/validator.ts index e2b5920067..62e517a757 100644 --- a/packages/validator-ajv8/src/validator.ts +++ b/packages/validator-ajv8/src/validator.ts @@ -1,30 +1,23 @@ import Ajv, { ErrorObject, ValidateFunction } from 'ajv'; -import get from 'lodash/get'; import { - createErrorHandler, CustomValidator, ErrorSchema, ErrorTransformer, FormContextType, - getDefaultFormState, - getUiOptions, - PROPERTIES_KEY, + ID_KEY, RJSFSchema, - RJSFValidationError, ROOT_SCHEMA_PREFIX, StrictRJSFSchema, toErrorList, - toErrorSchema, UiSchema, - unwrapErrorHandler, ValidationData, - validationDataMerge, ValidatorType, withIdRefPrefix, } from '@rjsf/utils'; import { CustomValidatorOptionsType, Localizer } from './types'; import createAjvInstance from './createAjvInstance'; +import processRawValidationErrors, { RawValidationErrorsType } from './processRawValidationErrors'; /** `ValidatorType` implementation that uses the AJV 8 validation mechanism. */ @@ -65,76 +58,17 @@ export default class AJV8Validator - ): RJSFValidationError[] { - return errors.map((e: ErrorObject) => { - const { instancePath, keyword, params, schemaPath, parentSchema, ...rest } = e; - let { message = '' } = rest; - let property = instancePath.replace(/\//g, '.'); - let stack = `${property} ${message}`.trim(); - - if ('missingProperty' in params) { - property = property ? `${property}.${params.missingProperty}` : params.missingProperty; - const currentProperty: string = params.missingProperty; - const uiSchemaTitle = getUiOptions(get(uiSchema, `${property.replace(/^\./, '')}`)).title; - - if (uiSchemaTitle) { - message = message.replace(currentProperty, uiSchemaTitle); - } else { - const parentSchemaTitle = get(parentSchema, [PROPERTIES_KEY, currentProperty, 'title']); - - if (parentSchemaTitle) { - message = message.replace(currentProperty, parentSchemaTitle); - } - } - - stack = message; - } else { - const uiSchemaTitle = getUiOptions(get(uiSchema, `${property.replace(/^\./, '')}`)).title; - - if (uiSchemaTitle) { - stack = `'${uiSchemaTitle}' ${message}`.trim(); - } else { - const parentSchemaTitle = parentSchema?.title; - - if (parentSchemaTitle) { - stack = `'${parentSchemaTitle}' ${message}`.trim(); - } - } - } - - // put data in expected format - return { - name: keyword, - property, - message, - params, // specific to ajv - stack, - schemaPath, - }; - }); - } - /** Runs the pure validation of the `schema` and `formData` without any of the RJSF functionality. Provided for use * by the playground. Returns the `errors` from the validation * * @param schema - The schema against which to validate the form data * @param schema * @param formData - The form data to validate */ - rawValidation(schema: RJSFSchema, formData?: T): { errors?: Result[]; validationError?: Error } { + rawValidation(schema: S, formData?: T): RawValidationErrorsType { let compilationError: Error | undefined = undefined; let compiledValidator: ValidateFunction | undefined; - if (schema['$id']) { - compiledValidator = this.ajv.getSchema(schema['$id']); + if (schema[ID_KEY]) { + compiledValidator = this.ajv.getSchema(schema[ID_KEY]); } try { if (compiledValidator === undefined) { @@ -181,37 +115,7 @@ export default class AJV8Validator ): ValidationData { const rawErrors = this.rawValidation(schema, formData); - const { validationError: invalidSchemaError } = rawErrors; - let errors = this.transformRJSFValidationErrors(rawErrors.errors, uiSchema); - - if (invalidSchemaError) { - errors = [...errors, { stack: invalidSchemaError!.message }]; - } - if (typeof transformErrors === 'function') { - errors = transformErrors(errors, uiSchema); - } - - let errorSchema = toErrorSchema(errors); - - if (invalidSchemaError) { - errorSchema = { - ...errorSchema, - $schema: { - __errors: [invalidSchemaError!.message], - }, - }; - } - - if (typeof customValidate !== 'function') { - return { errors, errorSchema }; - } - - // Include form data with undefined values, which is required for custom validation. - const newFormData = getDefaultFormState(this, schema, formData, schema, true) as T; - - const errorHandler = customValidate(newFormData, createErrorHandler(newFormData), uiSchema); - const userErrorSchema = unwrapErrorHandler(errorHandler); - return validationDataMerge({ errors, errorSchema }, userErrorSchema); + return processRawValidationErrors(this, rawErrors, formData, schema, customValidate, transformErrors, uiSchema); } /** Validates data against a schema, returning true if the data is valid, or @@ -223,7 +127,7 @@ export default class AJV8Validator(schema) as S; let compiledValidator: ValidateFunction | undefined; - if (schemaWithIdRefPrefix['$id']) { - compiledValidator = this.ajv.getSchema(schemaWithIdRefPrefix['$id']); + if (schemaWithIdRefPrefix[ID_KEY]) { + compiledValidator = this.ajv.getSchema(schemaWithIdRefPrefix[ID_KEY]); } if (compiledValidator === undefined) { compiledValidator = this.ajv.compile(schemaWithIdRefPrefix); diff --git a/packages/validator-ajv8/test/.eslintrc b/packages/validator-ajv8/test/.eslintrc index 796f216827..eb6417bd92 100644 --- a/packages/validator-ajv8/test/.eslintrc +++ b/packages/validator-ajv8/test/.eslintrc @@ -5,6 +5,9 @@ "globals": { "expect": true }, + "ignorePatterns": [ + "harness/*.js" + ], "rules": { "no-unused-vars": [ 2, diff --git a/packages/validator-ajv8/test/compileSchemaValidators.test.ts b/packages/validator-ajv8/test/compileSchemaValidators.test.ts new file mode 100644 index 0000000000..84f00db2f9 --- /dev/null +++ b/packages/validator-ajv8/test/compileSchemaValidators.test.ts @@ -0,0 +1,98 @@ +import { readFileSync, writeFileSync } from 'fs'; +import { RJSFSchema, schemaParser } from '@rjsf/utils'; + +import { compileSchemaValidators } from '../src'; +import createAjvInstance from '../src/createAjvInstance'; +import superSchema from './harness/superSchema.json'; +import { CUSTOM_OPTIONS } from './harness/testData'; + +jest.mock('fs', () => ({ + ...jest.requireActual('fs'), + writeFileSync: jest.fn(), +})); + +jest.mock('../src/createAjvInstance', () => + jest.fn().mockImplementation((...args) => jest.requireActual('../src/createAjvInstance').default(...args)) +); + +const OUTPUT_FILE = 'test.js'; + +describe('compileSchemaValidators()', () => { + let consoleLogSpy: jest.SpyInstance; + let expectedCode: string; + beforeAll(() => { + consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(); + }); + afterAll(() => { + consoleLogSpy.mockRestore(); + }); + describe('compiling without additional options', () => { + let schemas: RJSFSchema[]; + beforeAll(() => { + schemas = Object.values(schemaParser(superSchema as RJSFSchema)); + expectedCode = readFileSync('./test/harness/superSchema.js').toString(); + compileSchemaValidators(superSchema as RJSFSchema, OUTPUT_FILE); + }); + afterAll(() => { + consoleLogSpy.mockClear(); + (writeFileSync as jest.Mock).mockClear(); + }); + it('called console.log twice', () => { + expect(consoleLogSpy).toHaveBeenCalledTimes(2); + }); + it('the first time relates to parsing the schema', () => { + expect(consoleLogSpy).toHaveBeenNthCalledWith(1, 'parsing the schema'); + }); + it('the second time relates to writing the output file', () => { + expect(consoleLogSpy).toHaveBeenNthCalledWith(2, `writing ${OUTPUT_FILE}`); + }); + it('create AJV instance was called with the expected options', () => { + const expectedCompileOpts = { code: { source: true, lines: true }, schemas }; + expect(createAjvInstance).toHaveBeenCalledWith(undefined, undefined, expectedCompileOpts, undefined, undefined); + }); + it('wrote the expected output', () => { + expect(writeFileSync).toHaveBeenCalledWith(OUTPUT_FILE, expectedCode); + }); + }); + describe('compiling WITH additional options', () => { + let schemas: RJSFSchema[]; + beforeAll(() => { + schemas = Object.values(schemaParser(superSchema as RJSFSchema)); + expectedCode = readFileSync('./test/harness/superSchemaOptions.js').toString(); + compileSchemaValidators(superSchema as RJSFSchema, OUTPUT_FILE, CUSTOM_OPTIONS); + }); + afterAll(() => { + consoleLogSpy.mockClear(); + (writeFileSync as jest.Mock).mockClear(); + }); + it('called console.log twice', () => { + expect(consoleLogSpy).toHaveBeenCalledTimes(2); + }); + it('the first time relates to parsing the schema', () => { + expect(consoleLogSpy).toHaveBeenNthCalledWith(1, 'parsing the schema'); + }); + it('the second time relates to writing the output file', () => { + expect(consoleLogSpy).toHaveBeenNthCalledWith(2, `writing ${OUTPUT_FILE}`); + }); + it('create AJV instance was called with the expected options', () => { + const { + additionalMetaSchemas, + customFormats, + ajvOptionsOverrides = {}, + ajvFormatOptions, + AjvClass, + } = CUSTOM_OPTIONS; + const expectedCompileOpts = { ...ajvOptionsOverrides, code: { source: true, lines: true }, schemas }; + expect(createAjvInstance).toHaveBeenCalledWith( + additionalMetaSchemas, + customFormats, + expectedCompileOpts, + ajvFormatOptions, + AjvClass + ); + }); + it('wrote the expected output', () => { + expect(writeFileSync).toHaveBeenCalledWith(OUTPUT_FILE, expectedCode); + }); + }); +}); diff --git a/packages/validator-ajv8/test/createAjvInstance.test.ts b/packages/validator-ajv8/test/createAjvInstance.test.ts index f0da8b7391..a3a0afb559 100644 --- a/packages/validator-ajv8/test/createAjvInstance.test.ts +++ b/packages/validator-ajv8/test/createAjvInstance.test.ts @@ -3,27 +3,12 @@ import Ajv2019 from 'ajv/dist/2019'; import addFormats from 'ajv-formats'; import createAjvInstance, { AJV_CONFIG, COLOR_FORMAT_REGEX, DATA_URL_FORMAT_REGEX } from '../src/createAjvInstance'; -import { CustomValidatorOptionsType } from '../src'; +import { CUSTOM_OPTIONS } from './harness/testData'; jest.mock('ajv'); jest.mock('ajv/dist/2019'); jest.mock('ajv-formats'); -export const CUSTOM_OPTIONS: CustomValidatorOptionsType = { - additionalMetaSchemas: [require('ajv/lib/refs/json-schema-draft-06.json')], - customFormats: { - 'phone-us': /\(?\d{3}\)?[\s-]?\d{3}[\s-]?\d{4}$/, - 'area-code': /\d{3}/, - }, - ajvOptionsOverrides: { - $data: true, - verbose: true, - }, - ajvFormatOptions: { - mode: 'fast', - }, -}; - describe('createAjvInstance()', () => { describe('no additional meta schemas, custom formats, ajv options overrides or ajv format options', () => { let ajv: Ajv; diff --git a/packages/validator-ajv8/test/customizeValidator.test.ts b/packages/validator-ajv8/test/customizeValidator.test.ts index 42b39eba4d..4a3eb3a38c 100644 --- a/packages/validator-ajv8/test/customizeValidator.test.ts +++ b/packages/validator-ajv8/test/customizeValidator.test.ts @@ -1,6 +1,6 @@ import AJV8Validator from '../src/validator'; import defaultValidator, { customizeValidator, Localizer } from '../src'; -import { CUSTOM_OPTIONS } from './createAjvInstance.test'; +import { CUSTOM_OPTIONS } from './harness/testData'; jest.mock('../src/validator'); diff --git a/packages/validator-ajv8/test/harness/compileTestSchema.js b/packages/validator-ajv8/test/harness/compileTestSchema.js new file mode 100644 index 0000000000..c1e5b3e81b --- /dev/null +++ b/packages/validator-ajv8/test/harness/compileTestSchema.js @@ -0,0 +1,30 @@ +/** + * In order to keep things in sync, it may be necessary to run this after making changes in the schemaParser world in + * `@rjsf/utils` OR if an AJV update is installed. To run this, simply do the following, starting in the root directory + * of the `@rjsf/validator-ajv8` directory: + * + * - cd test/harness + * - node compileTestSchema.js + * + * Then add the two updated `superSchema.js` and `superSchemaOptions.js` files to your PR + */ + +const compileSchemaValidators = require('../../dist').compileSchemaValidators; +const superSchema = require('./superSchema.json'); + +// NOTE these are the same as the CUSTOM_OPTIONS in `testData.ts`, keep them in sync +const options = { + additionalMetaSchemas: [require('ajv/lib/refs/json-schema-draft-06.json')], + customFormats: { 'phone-us': /\(?\d{3}\)?[\s-]?\d{3}[\s-]?\d{4}$/, 'area-code': /\d{3}/ }, + ajvOptionsOverrides: { + $data: true, + verbose: true, + }, + ajvFormatOptions: { + mode: 'fast', + }, +}; + +compileSchemaValidators(superSchema, './superSchema.js'); + +compileSchemaValidators(superSchema, './superSchemaOptions.js', options); diff --git a/packages/validator-ajv8/test/harness/superSchema.js b/packages/validator-ajv8/test/harness/superSchema.js new file mode 100644 index 0000000000..e22bf45ea7 --- /dev/null +++ b/packages/validator-ajv8/test/harness/superSchema.js @@ -0,0 +1,732 @@ +"use strict"; +exports["-66914362"] = validate10; +const schema11 = {"definitions":{"foo":{"type":"object","properties":{"name":{"type":"string"}}},"price":{"title":"Price per task ($)","type":"number","multipleOf":0.03,"minimum":1},"passwords":{"type":"object","properties":{"pass1":{"type":"string"},"pass2":{"type":"string"}},"required":["pass1","pass2"]},"list":{"type":"array","items":{"type":"string"}},"choice1":{"type":"object","properties":{"choice":{"type":"string","const":"one"},"other":{"type":"number"}}},"choice2":{"type":"object","properties":{"choice":{"type":"string","const":"two"},"more":{"type":"string"}}}},"type":"object","properties":{"foo":{"type":"string"},"price":{"$ref":"#/definitions/price"},"passwords":{"$ref":"#/definitions/passwords"},"dataUrlWithName":{"type":"string","format":"data-url"},"phone":{"type":"string","format":"phone-us"},"multi":{"anyOf":[{"$ref":"#/definitions/foo"}]},"list":{"$ref":"#/definitions/list"},"single":{"oneOf":[{"$ref":"#/definitions/choice1"},{"$ref":"#/definitions/choice2"}]},"anything":{"type":"object","additionalProperties":{"type":"string"}}},"$id":"-66914362"}; +const schema12 = {"title":"Price per task ($)","type":"number","multipleOf":0.03,"minimum":1}; +const schema13 = {"type":"object","properties":{"pass1":{"type":"string"},"pass2":{"type":"string"}},"required":["pass1","pass2"]}; +const schema14 = {"type":"object","properties":{"name":{"type":"string"}}}; +const schema15 = {"type":"array","items":{"type":"string"}}; +const schema16 = {"type":"object","properties":{"choice":{"type":"string","const":"one"},"other":{"type":"number"}}}; +const schema17 = {"type":"object","properties":{"choice":{"type":"string","const":"two"},"more":{"type":"string"}}}; +const formats0 = /^data:([a-z]+\/[a-z0-9-+.]+)?;(?:name=(.*);)?base64,(.*)$/; + +function validate10(data, {instancePath="", parentData, parentDataProperty, rootData=data}={}){ +/*# sourceURL="-66914362" */; +let vErrors = null; +let errors = 0; +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.foo !== undefined){ +let data0 = data.foo; +if(typeof data0 !== "string"){ +const err0 = {instancePath:instancePath+"/foo",schemaPath:"#/properties/foo/type",keyword:"type",params:{type: "string"},message:"must be string",schema:schema11.properties.foo.type,parentSchema:schema11.properties.foo,data:data0}; +if(vErrors === null){ +vErrors = [err0]; +} +else { +vErrors.push(err0); +} +errors++; +} +} +if(data.price !== undefined){ +let data1 = data.price; +if(typeof data1 == "number"){ +if(data1 < 1 || isNaN(data1)){ +const err1 = {instancePath:instancePath+"/price",schemaPath:"#/definitions/price/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1",schema:1,parentSchema:schema12,data:data1}; +if(vErrors === null){ +vErrors = [err1]; +} +else { +vErrors.push(err1); +} +errors++; +} +let res0; +if((0.03 === 0 || (res0 = data1/0.03, Math.abs(Math.round(res0) - res0) > 1e-8))){ +const err2 = {instancePath:instancePath+"/price",schemaPath:"#/definitions/price/multipleOf",keyword:"multipleOf",params:{multipleOf: 0.03},message:"must be multiple of 0.03",schema:0.03,parentSchema:schema12,data:data1}; +if(vErrors === null){ +vErrors = [err2]; +} +else { +vErrors.push(err2); +} +errors++; +} +} +else { +const err3 = {instancePath:instancePath+"/price",schemaPath:"#/definitions/price/type",keyword:"type",params:{type: "number"},message:"must be number",schema:schema12.type,parentSchema:schema12,data:data1}; +if(vErrors === null){ +vErrors = [err3]; +} +else { +vErrors.push(err3); +} +errors++; +} +} +if(data.passwords !== undefined){ +let data2 = data.passwords; +if(data2 && typeof data2 == "object" && !Array.isArray(data2)){ +if(data2.pass1 === undefined){ +const err4 = {instancePath:instancePath+"/passwords",schemaPath:"#/definitions/passwords/required",keyword:"required",params:{missingProperty: "pass1"},message:"must have required property '"+"pass1"+"'",schema:schema13.required,parentSchema:schema13,data:data2}; +if(vErrors === null){ +vErrors = [err4]; +} +else { +vErrors.push(err4); +} +errors++; +} +if(data2.pass2 === undefined){ +const err5 = {instancePath:instancePath+"/passwords",schemaPath:"#/definitions/passwords/required",keyword:"required",params:{missingProperty: "pass2"},message:"must have required property '"+"pass2"+"'",schema:schema13.required,parentSchema:schema13,data:data2}; +if(vErrors === null){ +vErrors = [err5]; +} +else { +vErrors.push(err5); +} +errors++; +} +if(data2.pass1 !== undefined){ +let data3 = data2.pass1; +if(typeof data3 !== "string"){ +const err6 = {instancePath:instancePath+"/passwords/pass1",schemaPath:"#/definitions/passwords/properties/pass1/type",keyword:"type",params:{type: "string"},message:"must be string",schema:schema13.properties.pass1.type,parentSchema:schema13.properties.pass1,data:data3}; +if(vErrors === null){ +vErrors = [err6]; +} +else { +vErrors.push(err6); +} +errors++; +} +} +if(data2.pass2 !== undefined){ +let data4 = data2.pass2; +if(typeof data4 !== "string"){ +const err7 = {instancePath:instancePath+"/passwords/pass2",schemaPath:"#/definitions/passwords/properties/pass2/type",keyword:"type",params:{type: "string"},message:"must be string",schema:schema13.properties.pass2.type,parentSchema:schema13.properties.pass2,data:data4}; +if(vErrors === null){ +vErrors = [err7]; +} +else { +vErrors.push(err7); +} +errors++; +} +} +} +else { +const err8 = {instancePath:instancePath+"/passwords",schemaPath:"#/definitions/passwords/type",keyword:"type",params:{type: "object"},message:"must be object",schema:schema13.type,parentSchema:schema13,data:data2}; +if(vErrors === null){ +vErrors = [err8]; +} +else { +vErrors.push(err8); +} +errors++; +} +} +if(data.dataUrlWithName !== undefined){ +let data5 = data.dataUrlWithName; +if(typeof data5 === "string"){ +if(!(formats0.test(data5))){ +const err9 = {instancePath:instancePath+"/dataUrlWithName",schemaPath:"#/properties/dataUrlWithName/format",keyword:"format",params:{format: "data-url"},message:"must match format \""+"data-url"+"\"",schema:"data-url",parentSchema:schema11.properties.dataUrlWithName,data:data5}; +if(vErrors === null){ +vErrors = [err9]; +} +else { +vErrors.push(err9); +} +errors++; +} +} +else { +const err10 = {instancePath:instancePath+"/dataUrlWithName",schemaPath:"#/properties/dataUrlWithName/type",keyword:"type",params:{type: "string"},message:"must be string",schema:schema11.properties.dataUrlWithName.type,parentSchema:schema11.properties.dataUrlWithName,data:data5}; +if(vErrors === null){ +vErrors = [err10]; +} +else { +vErrors.push(err10); +} +errors++; +} +} +if(data.phone !== undefined){ +let data6 = data.phone; +if(!(typeof data6 === "string")){ +const err11 = {instancePath:instancePath+"/phone",schemaPath:"#/properties/phone/type",keyword:"type",params:{type: "string"},message:"must be string",schema:schema11.properties.phone.type,parentSchema:schema11.properties.phone,data:data6}; +if(vErrors === null){ +vErrors = [err11]; +} +else { +vErrors.push(err11); +} +errors++; +} +} +if(data.multi !== undefined){ +let data7 = data.multi; +const _errs18 = errors; +let valid4 = false; +const _errs19 = errors; +if(data7 && typeof data7 == "object" && !Array.isArray(data7)){ +if(data7.name !== undefined){ +let data8 = data7.name; +if(typeof data8 !== "string"){ +const err12 = {instancePath:instancePath+"/multi/name",schemaPath:"#/definitions/foo/properties/name/type",keyword:"type",params:{type: "string"},message:"must be string",schema:schema14.properties.name.type,parentSchema:schema14.properties.name,data:data8}; +if(vErrors === null){ +vErrors = [err12]; +} +else { +vErrors.push(err12); +} +errors++; +} +} +} +else { +const err13 = {instancePath:instancePath+"/multi",schemaPath:"#/definitions/foo/type",keyword:"type",params:{type: "object"},message:"must be object",schema:schema14.type,parentSchema:schema14,data:data7}; +if(vErrors === null){ +vErrors = [err13]; +} +else { +vErrors.push(err13); +} +errors++; +} +var _valid0 = _errs19 === errors; +valid4 = valid4 || _valid0; +if(!valid4){ +const err14 = {instancePath:instancePath+"/multi",schemaPath:"#/properties/multi/anyOf",keyword:"anyOf",params:{},message:"must match a schema in anyOf",schema:schema11.properties.multi.anyOf,parentSchema:schema11.properties.multi,data:data7}; +if(vErrors === null){ +vErrors = [err14]; +} +else { +vErrors.push(err14); +} +errors++; +} +else { +errors = _errs18; +if(vErrors !== null){ +if(_errs18){ +vErrors.length = _errs18; +} +else { +vErrors = null; +} +} +} +} +if(data.list !== undefined){ +let data9 = data.list; +if(Array.isArray(data9)){ +const len0 = data9.length; +for(let i0=0; i0=", limit: 1},message:"must be >= 1",schema:1,parentSchema:schema39,data:data1}; +if(vErrors === null){ +vErrors = [err1]; +} +else { +vErrors.push(err1); +} +errors++; +} +let res0; +if((0.03 === 0 || (res0 = data1/0.03, Math.abs(Math.round(res0) - res0) > 1e-8))){ +const err2 = {instancePath:instancePath+"/price",schemaPath:"#/definitions/price/multipleOf",keyword:"multipleOf",params:{multipleOf: 0.03},message:"must be multiple of 0.03",schema:0.03,parentSchema:schema39,data:data1}; +if(vErrors === null){ +vErrors = [err2]; +} +else { +vErrors.push(err2); +} +errors++; +} +} +else { +const err3 = {instancePath:instancePath+"/price",schemaPath:"#/definitions/price/type",keyword:"type",params:{type: "number"},message:"must be number",schema:schema39.type,parentSchema:schema39,data:data1}; +if(vErrors === null){ +vErrors = [err3]; +} +else { +vErrors.push(err3); +} +errors++; +} +} +if(data.passwords !== undefined){ +let data2 = data.passwords; +if(data2 && typeof data2 == "object" && !Array.isArray(data2)){ +if(data2.pass1 === undefined){ +const err4 = {instancePath:instancePath+"/passwords",schemaPath:"#/definitions/passwords/required",keyword:"required",params:{missingProperty: "pass1"},message:"must have required property '"+"pass1"+"'",schema:schema40.required,parentSchema:schema40,data:data2}; +if(vErrors === null){ +vErrors = [err4]; +} +else { +vErrors.push(err4); +} +errors++; +} +if(data2.pass2 === undefined){ +const err5 = {instancePath:instancePath+"/passwords",schemaPath:"#/definitions/passwords/required",keyword:"required",params:{missingProperty: "pass2"},message:"must have required property '"+"pass2"+"'",schema:schema40.required,parentSchema:schema40,data:data2}; +if(vErrors === null){ +vErrors = [err5]; +} +else { +vErrors.push(err5); +} +errors++; +} +if(data2.pass1 !== undefined){ +let data3 = data2.pass1; +if(typeof data3 !== "string"){ +const err6 = {instancePath:instancePath+"/passwords/pass1",schemaPath:"#/definitions/passwords/properties/pass1/type",keyword:"type",params:{type: "string"},message:"must be string",schema:schema40.properties.pass1.type,parentSchema:schema40.properties.pass1,data:data3}; +if(vErrors === null){ +vErrors = [err6]; +} +else { +vErrors.push(err6); +} +errors++; +} +} +if(data2.pass2 !== undefined){ +let data4 = data2.pass2; +if(typeof data4 !== "string"){ +const err7 = {instancePath:instancePath+"/passwords/pass2",schemaPath:"#/definitions/passwords/properties/pass2/type",keyword:"type",params:{type: "string"},message:"must be string",schema:schema40.properties.pass2.type,parentSchema:schema40.properties.pass2,data:data4}; +if(vErrors === null){ +vErrors = [err7]; +} +else { +vErrors.push(err7); +} +errors++; +} +} +} +else { +const err8 = {instancePath:instancePath+"/passwords",schemaPath:"#/definitions/passwords/type",keyword:"type",params:{type: "object"},message:"must be object",schema:schema40.type,parentSchema:schema40,data:data2}; +if(vErrors === null){ +vErrors = [err8]; +} +else { +vErrors.push(err8); +} +errors++; +} +} +if(data.dataUrlWithName !== undefined){ +let data5 = data.dataUrlWithName; +if(typeof data5 === "string"){ +if(!(formats0.test(data5))){ +const err9 = {instancePath:instancePath+"/dataUrlWithName",schemaPath:"#/properties/dataUrlWithName/format",keyword:"format",params:{format: "data-url"},message:"must match format \""+"data-url"+"\"",schema:"data-url",parentSchema:schema38.properties.dataUrlWithName,data:data5}; +if(vErrors === null){ +vErrors = [err9]; +} +else { +vErrors.push(err9); +} +errors++; +} +} +else { +const err10 = {instancePath:instancePath+"/dataUrlWithName",schemaPath:"#/properties/dataUrlWithName/type",keyword:"type",params:{type: "string"},message:"must be string",schema:schema38.properties.dataUrlWithName.type,parentSchema:schema38.properties.dataUrlWithName,data:data5}; +if(vErrors === null){ +vErrors = [err10]; +} +else { +vErrors.push(err10); +} +errors++; +} +} +if(data.phone !== undefined){ +let data6 = data.phone; +if(typeof data6 === "string"){ +if(!(formats2.test(data6))){ +const err11 = {instancePath:instancePath+"/phone",schemaPath:"#/properties/phone/format",keyword:"format",params:{format: "phone-us"},message:"must match format \""+"phone-us"+"\"",schema:"phone-us",parentSchema:schema38.properties.phone,data:data6}; +if(vErrors === null){ +vErrors = [err11]; +} +else { +vErrors.push(err11); +} +errors++; +} +} +else { +const err12 = {instancePath:instancePath+"/phone",schemaPath:"#/properties/phone/type",keyword:"type",params:{type: "string"},message:"must be string",schema:schema38.properties.phone.type,parentSchema:schema38.properties.phone,data:data6}; +if(vErrors === null){ +vErrors = [err12]; +} +else { +vErrors.push(err12); +} +errors++; +} +} +if(data.multi !== undefined){ +let data7 = data.multi; +const _errs18 = errors; +let valid4 = false; +const _errs19 = errors; +if(data7 && typeof data7 == "object" && !Array.isArray(data7)){ +if(data7.name !== undefined){ +let data8 = data7.name; +if(typeof data8 !== "string"){ +const err13 = {instancePath:instancePath+"/multi/name",schemaPath:"#/definitions/foo/properties/name/type",keyword:"type",params:{type: "string"},message:"must be string",schema:schema41.properties.name.type,parentSchema:schema41.properties.name,data:data8}; +if(vErrors === null){ +vErrors = [err13]; +} +else { +vErrors.push(err13); +} +errors++; +} +} +} +else { +const err14 = {instancePath:instancePath+"/multi",schemaPath:"#/definitions/foo/type",keyword:"type",params:{type: "object"},message:"must be object",schema:schema41.type,parentSchema:schema41,data:data7}; +if(vErrors === null){ +vErrors = [err14]; +} +else { +vErrors.push(err14); +} +errors++; +} +var _valid0 = _errs19 === errors; +valid4 = valid4 || _valid0; +if(!valid4){ +const err15 = {instancePath:instancePath+"/multi",schemaPath:"#/properties/multi/anyOf",keyword:"anyOf",params:{},message:"must match a schema in anyOf",schema:schema38.properties.multi.anyOf,parentSchema:schema38.properties.multi,data:data7}; +if(vErrors === null){ +vErrors = [err15]; +} +else { +vErrors.push(err15); +} +errors++; +} +else { +errors = _errs18; +if(vErrors !== null){ +if(_errs18){ +vErrors.length = _errs18; +} +else { +vErrors = null; +} +} +} +} +if(data.list !== undefined){ +let data9 = data.list; +if(Array.isArray(data9)){ +const len0 = data9.length; +for(let i0=0; i0 { + let builder: ErrorSchemaBuilder; + beforeAll(() => { + builder = new ErrorSchemaBuilder(); + }); + afterEach(() => { + builder.resetAllErrors(); + }); + describe('default options', () => { + // Use the AJV8PrecompiledValidator + let validator: AJV8PrecompiledValidator; + beforeAll(() => { + validator = new AJV8PrecompiledValidator(validateFns, rootSchema); + }); + describe('validator.isValid()', () => { + it('should return true if the data is valid against the schema', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + name: { type: 'string' }, + }, + anyOf: [ + { + required: ['name'], + }, + ], + }; + + expect(validator.isValid(schema, { name: 'bar' }, rootSchema)).toBe(true); + }); + it('should return false if the data is not valid against the schema', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + name: { type: 'string' }, + }, + anyOf: [ + { + required: ['name'], + }, + ], + }; + + expect(validator.isValid(schema, { name: 12345 }, rootSchema)).toBe(false); + }); + it('should throw if the schema is not recognized', () => { + const schema: RJSFSchema = 'foobarbaz' as unknown as RJSFSchema; + const hash = hashForSchema(schema); + expect(() => validator.isValid(schema, { name: 'bar' }, rootSchema)).toThrowError( + new Error(`No precompiled validator function was found for the given schema for "${hash}"`) + ); + }); + it('should throw if the rootSchema is different than the one the validator was constructed with', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + name: { type: 'string' }, + }, + }; + expect(() => validator.isValid(schema, { foo: { name: 'bar' } }, schema)).toThrowError( + new Error( + 'The schema associated with the precompiled validator differs from the rootSchema provided for validation' + ) + ); + }); + }); + describe('validator.toErrorList()', () => { + it('should return empty list for unspecified errorSchema', () => { + expect(validator.toErrorList()).toEqual([]); + }); + it('should convert an errorSchema into a flat list', () => { + const errorSchema = builder + .addErrors(['err1', 'err2']) + .addErrors(['err3', 'err4'], 'a.b') + .addErrors(['err5'], 'c').ErrorSchema; + expect(validator.toErrorList(errorSchema)).toEqual([ + { property: '.', message: 'err1', stack: '. err1' }, + { property: '.', message: 'err2', stack: '. err2' }, + { property: '.a.b', message: 'err3', stack: '.a.b err3' }, + { property: '.a.b', message: 'err4', stack: '.a.b err4' }, + { property: '.c', message: 'err5', stack: '.c err5' }, + ]); + }); + }); + describe('validator.validateFormData()', () => { + it('throws an error when the schemas differ', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + name: { type: 'string' }, + }, + }; + expect(() => validator.validateFormData({}, schema)).toThrowError( + new Error('The schema associated with the precompiled schema differs from the schema provided for validation') + ); + }); + describe('No custom validate function, single value', () => { + let errors: RJSFValidationError[]; + let errorSchema: ErrorSchema; + + beforeAll(() => { + const result = validator.validateFormData({ foo: 42 }, rootSchema); + errors = result.errors; + errorSchema = result.errorSchema; + }); + + it('should return an error list', () => { + expect(errors).toHaveLength(1); + expect(errors[0].message).toEqual('must be string'); + }); + it('should return an errorSchema', () => { + expect(errorSchema.foo!.__errors).toHaveLength(1); + expect(errorSchema.foo!.__errors![0]).toEqual('must be string'); + }); + }); + describe('Validating multipleOf with a float', () => { + let errors: RJSFValidationError[]; + beforeAll(() => { + const result = validator.validateFormData({ price: 1.05 }, rootSchema); + errors = result.errors; + }); + it('should not return an error', () => { + expect(errors).toHaveLength(0); + }); + }); + describe('Validating multipleOf with a float, with multiple errors', () => { + let errors: RJSFValidationError[]; + let errorSchema: ErrorSchema; + beforeAll(() => { + const result = validator.validateFormData({ price: 0.14 }, rootSchema); + errors = result.errors; + errorSchema = result.errorSchema; + }); + it('should have 2 errors', () => { + expect(errors).toHaveLength(2); + }); + it('first error is for minimum', () => { + expect(errors[0].message).toEqual('must be >= 1'); + }); + it('first error is for multipleOf', () => { + expect(errors[1].message).toEqual('must be multiple of 0.03'); + }); + it('should return an errorSchema', () => { + expect(errorSchema.price!.__errors).toHaveLength(2); + expect(errorSchema.price!.__errors).toEqual(['must be >= 1', 'must be multiple of 0.03']); + }); + }); + describe('Validating required fields', () => { + let errors: RJSFValidationError[]; + let errorSchema: ErrorSchema; + describe('formData is not provided at top level', () => { + beforeAll(() => { + const formData = { passwords: { pass1: 'a' } }; + const result = validator.validateFormData(formData, rootSchema); + errors = result.errors; + errorSchema = result.errorSchema; + }); + it('should return an error list', () => { + expect(errors).toHaveLength(1); + expect(errors[0].stack).toEqual("must have required property 'pass2'"); + }); + it('should return an errorSchema', () => { + expect(errorSchema.passwords!.pass2!.__errors).toHaveLength(1); + expect(errorSchema.passwords!.pass2!.__errors![0]).toEqual("must have required property 'pass2'"); + }); + }); + }); + describe('No custom validate function, single additionalProperties value', () => { + let errors: RJSFValidationError[]; + let errorSchema: ErrorSchema; + + beforeAll(() => { + const result = validator.validateFormData({ anything: { foo: 42 } }, rootSchema); + errors = result.errors; + errorSchema = result.errorSchema; + }); + + it('should return an error list', () => { + expect(errors).toHaveLength(1); + expect(errors[0].message).toEqual('must be string'); + }); + it('should return an errorSchema', () => { + expect(errorSchema.anything!.foo!.__errors).toHaveLength(1); + expect(errorSchema.anything!.foo!.__errors![0]).toEqual('must be string'); + }); + }); + describe('TransformErrors', () => { + let errors: RJSFValidationError[]; + let newErrorMessage: string; + let transformErrors: jest.Mock; + let uiSchema: UiSchema; + beforeAll(() => { + uiSchema = { + name: { 'ui:label': false }, + }; + newErrorMessage = 'Better error message'; + transformErrors = jest.fn((errors: RJSFValidationError[]) => { + return [Object.assign({}, errors[0], { message: newErrorMessage })]; + }); + const result = validator.validateFormData({ name: 42 }, rootSchema, undefined, transformErrors, uiSchema); + errors = result.errors; + }); + + it('should use transformErrors function', () => { + expect(errors).not.toHaveLength(0); + expect(errors[0].message).toEqual(newErrorMessage); + }); + it('transformErrors function was called with uiSchema', () => { + expect(transformErrors).toHaveBeenCalledWith(expect.any(Array), uiSchema); + }); + }); + describe('Custom validate function', () => { + let errors: RJSFValidationError[]; + let errorSchema: ErrorSchema; + let validate: jest.Mock; + let uiSchema: UiSchema; + beforeAll(() => { + uiSchema = { + name: { 'ui:label': false }, + }; + + validate = jest.fn((formData: any, errors: FormValidation) => { + if (formData.passwords.pass1 !== formData.passwords.pass2) { + errors.passwords!.pass2!.addError('passwords don`t match.'); + } + return errors; + }); + }); + describe('formData is provided', () => { + beforeAll(() => { + const formData = { passwords: { pass1: 'a', pass2: 'b' } }; + const result = validator.validateFormData(formData, rootSchema, validate, undefined, uiSchema); + errors = result.errors; + errorSchema = result.errorSchema; + }); + it('should return an error list', () => { + expect(errors).toHaveLength(1); + expect(errors[0].stack).toEqual('.passwords.pass2 passwords don`t match.'); + }); + it('should return an errorSchema', () => { + expect(errorSchema.passwords!.pass2!.__errors).toHaveLength(1); + expect(errorSchema.passwords!.pass2!.__errors![0]).toEqual('passwords don`t match.'); + }); + it('validate function was called with uiSchema', () => { + expect(validate).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), uiSchema); + }); + }); + describe('formData is missing data', () => { + beforeAll(() => { + const formData = { passwords: { pass1: 'a' } }; + const result = validator.validateFormData(formData, rootSchema, validate); + errors = result.errors; + errorSchema = result.errorSchema; + }); + it('should return an error list', () => { + expect(errors).toHaveLength(2); + expect(errors[0].stack).toEqual("must have required property 'pass2'"); + expect(errors[1].stack).toEqual('.passwords.pass2 passwords don`t match.'); + }); + it('should return an errorSchema', () => { + expect(errorSchema.passwords!.pass2!.__errors).toHaveLength(2); + expect(errorSchema.passwords!.pass2!.__errors![0]).toEqual("must have required property 'pass2'"); + expect(errorSchema.passwords!.pass2!.__errors![1]).toEqual('passwords don`t match.'); + }); + }); + }); + describe('Data-Url validation', () => { + it('Data-Url with name is accepted', () => { + const formData = { + dataUrlWithName: 'data:text/plain;name=file1.txt;base64,x=', + }; + const result = validator.validateFormData(formData, rootSchema); + expect(result.errors).toHaveLength(0); + }); + it('Data-Url without name is accepted', () => { + const formData = { + dataUrlWithName: 'data:text/plain;base64,x=', + }; + const result = validator.validateFormData(formData, rootSchema); + expect(result.errors).toHaveLength(0); + }); + }); + }); + }); + describe('validator.validateFormData(), custom options, and localizer', () => { + let validator: AJV8PrecompiledValidator; + let localizer: Localizer; + beforeAll(() => { + localizer = jest.fn().mockImplementation(); + validator = new AJV8PrecompiledValidator(validateOptionsFns, rootSchema, localizer); + }); + describe('validating using single custom meta schema', () => { + let errors: RJSFValidationError[]; + beforeAll(() => { + (localizer as jest.Mock).mockClear(); + const result = validator.validateFormData({ foo: 42 }, rootSchema); + errors = result.errors; + }); + it('should return 1 error about formData', () => { + expect(errors).toHaveLength(1); + }); + it('has a pattern match validation error about formData', () => { + expect(errors[0].stack).toEqual('.foo must be string'); + }); + it('localizer was called with the errors', () => { + expect(localizer).toHaveBeenCalledWith([ + { + data: 42, + instancePath: '/foo', + keyword: 'type', + message: 'must be string', + params: { + type: 'string', + }, + parentSchema: { + type: 'string', + }, + schema: 'string', + schemaPath: '#/properties/foo/type', + }, + ]); + }); + }); + describe('validating using custom string formats', () => { + it('should not return a validation error if proper string format is used', () => { + const result = validator.validateFormData({ phone: '800-555-2368' }, rootSchema); + expect(result.errors).toHaveLength(0); + }); + describe('validating using a custom formats', () => { + let errors: RJSFValidationError[]; + + beforeAll(() => { + const result = validator.validateFormData({ phone: '800.555.2368' }, rootSchema); + errors = result.errors; + }); + it('should return 1 error about formData', () => { + expect(errors).toHaveLength(1); + }); + it('should return a validation error about formData', () => { + expect(errors[0].stack).toEqual('.phone must match format "phone-us"'); + }); + }); + }); + }); +}); diff --git a/packages/validator-ajv8/test/processRawValidationErrors.test.ts b/packages/validator-ajv8/test/processRawValidationErrors.test.ts new file mode 100644 index 0000000000..7bb7eff793 --- /dev/null +++ b/packages/validator-ajv8/test/processRawValidationErrors.test.ts @@ -0,0 +1,19 @@ +import { transformRJSFValidationErrors } from '../src/processRawValidationErrors'; + +describe('transformRJSFValidationErrors', () => { + // The rest of this function is tested by the validators + it('should transform errors without an error message or parentSchema field', () => { + const error = { + instancePath: '/numberOfChildren', + schemaPath: '#/properties/numberOfChildren/pattern', + keyword: 'pattern', + params: { pattern: '\\d+' }, + schema: '\\d+', + data: 'aa', + }; + + const errors = transformRJSFValidationErrors([error]); + + expect(errors).toHaveLength(1); + }); +}); diff --git a/packages/validator-ajv8/test/usePrecompiledValidator.test.ts b/packages/validator-ajv8/test/usePrecompiledValidator.test.ts new file mode 100644 index 0000000000..cb8cc24180 --- /dev/null +++ b/packages/validator-ajv8/test/usePrecompiledValidator.test.ts @@ -0,0 +1,48 @@ +import { RJSFSchema } from '@rjsf/utils'; + +import AJV8PrecompiledValidator from '../src/precompiledValidator'; +import { Localizer, ValidatorFunctions } from '../src'; +import * as superSchemaFns from './harness/superSchema'; +import superSchema from './harness/superSchema.json'; +import usePrecompiledValidator from '../src/usePrecompiledValidator'; + +jest.mock('../src/precompiledValidator'); + +type TestType = { + foo: string; + bar: boolean; +}; + +const validateFns = superSchemaFns as ValidatorFunctions; +const rootSchema = superSchema as RJSFSchema; + +describe('usePrecompiledValidator()', () => { + describe('passing validatorFns and rootSchema to usePrecompiledValidator', () => { + let custom: any; + beforeAll(() => { + (AJV8PrecompiledValidator as unknown as jest.Mock).mockClear(); + custom = usePrecompiledValidator(validateFns, rootSchema); + }); + it('precompiled validator was created', () => { + expect(custom).toBeInstanceOf(AJV8PrecompiledValidator); + }); + it('precompiledValidator was constructed with validateFns and rootSchema', () => { + expect(AJV8PrecompiledValidator).toHaveBeenCalledWith(validateFns, rootSchema, undefined); + }); + }); + describe('passing validatorFns, rootSchema and localizer to usePrecompiledValidator', () => { + let custom: any; + let localizer: Localizer; + beforeAll(() => { + localizer = jest.fn(); + (AJV8PrecompiledValidator as unknown as jest.Mock).mockClear(); + custom = usePrecompiledValidator(validateFns, rootSchema, localizer); + }); + it('precompiled validator was created', () => { + expect(custom).toBeInstanceOf(AJV8PrecompiledValidator); + }); + it('defaultValidator was constructed with validateFns, rootSchema and the localizer', () => { + expect(AJV8PrecompiledValidator).toHaveBeenCalledWith(validateFns, rootSchema, localizer); + }); + }); +}); diff --git a/packages/validator-ajv8/test/validator.test.ts b/packages/validator-ajv8/test/validator.test.ts index edbc6324d8..e9a0c3bbdf 100644 --- a/packages/validator-ajv8/test/validator.test.ts +++ b/packages/validator-ajv8/test/validator.test.ts @@ -1,4 +1,3 @@ -import { ErrorObject } from 'ajv'; import Ajv2019 from 'ajv/dist/2019'; import Ajv2020 from 'ajv/dist/2020'; import { @@ -14,12 +13,6 @@ import { import AJV8Validator from '../src/validator'; import { Localizer } from '../src'; -class TestValidator extends AJV8Validator { - transformRJSFValidationErrors(errors: ErrorObject[] = [], uiSchema?: UiSchema): RJSFValidationError[] { - return super.transformRJSFValidationErrors(errors, uiSchema); - } -} - const illFormedKey = "bar`'()=+*&^%$#@!"; const metaSchemaDraft6 = require('ajv/lib/refs/json-schema-draft-06.json'); @@ -32,10 +25,10 @@ describe('AJV8Validator', () => { builder.resetAllErrors(); }); describe('default options', () => { - // Use the TestValidator to access the `withIdRefPrefix` function - let validator: TestValidator; + // Use the AJV8Validator to access the `withIdRefPrefix` function + let validator: AJV8Validator; beforeAll(() => { - validator = new TestValidator({}); + validator = new AJV8Validator({}); }); describe('validator.isValid()', () => { it('should return true if the data is valid against the schema', () => { @@ -486,10 +479,10 @@ describe('AJV8Validator', () => { }); }); describe('default options, with Ajv2019', () => { - // Use the TestValidator to access the `withIdRefPrefix` function - let validator: TestValidator; + // Use the AJV8Validator to access the `withIdRefPrefix` function + let validator: AJV8Validator; beforeAll(() => { - validator = new TestValidator({ AjvClass: Ajv2019 }); + validator = new AJV8Validator({ AjvClass: Ajv2019 }); }); describe('validator.isValid()', () => { it('should return true if the data is valid against the schema', () => { @@ -941,10 +934,10 @@ describe('AJV8Validator', () => { }); }); describe('default options, with Ajv2020', () => { - // Use the TestValidator to access the `withIdRefPrefix` function - let validator: TestValidator; + // Use the AJV8Validator to access the `withIdRefPrefix` function + let validator: AJV8Validator; beforeAll(() => { - validator = new TestValidator({ AjvClass: Ajv2020 }); + validator = new AJV8Validator({ AjvClass: Ajv2020 }); }); describe('validator.isValid()', () => { it('should return true if the data is valid against the schema', () => { @@ -1369,22 +1362,6 @@ describe('AJV8Validator', () => { }); }); }); - describe('passing optional error fields to transformRJSFValidationErrors', () => { - it('should transform errors without an error message or parentSchema field', () => { - const error = { - instancePath: '/numberOfChildren', - schemaPath: '#/properties/numberOfChildren/pattern', - keyword: 'pattern', - params: { pattern: '\\d+' }, - schema: '\\d+', - data: 'aa', - }; - - const errors = validator.transformRJSFValidationErrors([error]); - - expect(errors).toHaveLength(1); - }); - }); describe('No custom validate function, single additionalProperties value', () => { let errors: RJSFValidationError[]; let errorSchema: ErrorSchema; @@ -1600,12 +1577,12 @@ describe('AJV8Validator', () => { }); }); describe('validator.validateFormData(), custom options, and localizer', () => { - let validator: TestValidator; + let validator: AJV8Validator; let schema: RJSFSchema; let localizer: Localizer; beforeAll(() => { localizer = jest.fn().mockImplementation(); - validator = new TestValidator({}, localizer); + validator = new AJV8Validator({}, localizer); schema = { $ref: '#/definitions/Dataset', $schema: 'http://json-schema.org/draft-06/schema#', @@ -1636,7 +1613,7 @@ describe('AJV8Validator', () => { let errors: RJSFValidationError[]; beforeAll(() => { (localizer as jest.Mock).mockClear(); - validator = new TestValidator( + validator = new AJV8Validator( { additionalMetaSchemas: [metaSchemaDraft6], }, @@ -1673,7 +1650,7 @@ describe('AJV8Validator', () => { let errors: RJSFValidationError[]; beforeAll(() => { - validator = new TestValidator({ + validator = new AJV8Validator({ additionalMetaSchemas: [metaSchemaDraft6], }); const result = validator.validateFormData({ datasetId: 'some kind of text' }, schema); @@ -1752,12 +1729,12 @@ describe('AJV8Validator', () => { }); }); describe('validator.validateFormData(), custom options, localizer and Ajv2019', () => { - let validator: TestValidator; + let validator: AJV8Validator; let schema: RJSFSchema; let localizer: Localizer; beforeAll(() => { localizer = jest.fn().mockImplementation(); - validator = new TestValidator({ AjvClass: Ajv2019 }, localizer); + validator = new AJV8Validator({ AjvClass: Ajv2019 }, localizer); schema = { $ref: '#/definitions/Dataset', $schema: 'http://json-schema.org/draft-06/schema#', @@ -1788,7 +1765,7 @@ describe('AJV8Validator', () => { let errors: RJSFValidationError[]; beforeAll(() => { (localizer as jest.Mock).mockClear(); - validator = new TestValidator( + validator = new AJV8Validator( { additionalMetaSchemas: [metaSchemaDraft6], AjvClass: Ajv2019, @@ -1826,7 +1803,7 @@ describe('AJV8Validator', () => { let errors: RJSFValidationError[]; beforeAll(() => { - validator = new TestValidator({ + validator = new AJV8Validator({ additionalMetaSchemas: [metaSchemaDraft6], AjvClass: Ajv2019, }); @@ -1906,12 +1883,12 @@ describe('AJV8Validator', () => { }); }); describe('validator.validateFormData(), custom options, localizer and Ajv2020', () => { - let validator: TestValidator; + let validator: AJV8Validator; let schema: RJSFSchema; let localizer: Localizer; beforeAll(() => { localizer = jest.fn().mockImplementation(); - validator = new TestValidator({ AjvClass: Ajv2020 }, localizer); + validator = new AJV8Validator({ AjvClass: Ajv2020 }, localizer); schema = { $ref: '#/definitions/Dataset', $schema: 'http://json-schema.org/draft-06/schema#', @@ -1942,7 +1919,7 @@ describe('AJV8Validator', () => { let errors: RJSFValidationError[]; beforeAll(() => { (localizer as jest.Mock).mockClear(); - validator = new TestValidator( + validator = new AJV8Validator( { additionalMetaSchemas: [metaSchemaDraft6], AjvClass: Ajv2020, @@ -1980,7 +1957,7 @@ describe('AJV8Validator', () => { let errors: RJSFValidationError[]; beforeAll(() => { - validator = new TestValidator({ + validator = new AJV8Validator({ additionalMetaSchemas: [metaSchemaDraft6], AjvClass: Ajv2020, }); diff --git a/packages/validator-ajv8/tsconfig.json b/packages/validator-ajv8/tsconfig.json index def2798225..a02af52e11 100644 --- a/packages/validator-ajv8/tsconfig.json +++ b/packages/validator-ajv8/tsconfig.json @@ -3,6 +3,7 @@ "include": ["src", "types"], "compilerOptions": { "rootDir": "./src", + "resolveJsonModule": true, "baseUrl": "./", "paths": { "*": ["src/*", "node_modules/*"]