diff --git a/src/lib/unstable/core/SchemaRendererServiceField/types.ts b/src/lib/unstable/core/SchemaRendererServiceField/types.ts index 23a95fa3..41979be7 100644 --- a/src/lib/unstable/core/SchemaRendererServiceField/types.ts +++ b/src/lib/unstable/core/SchemaRendererServiceField/types.ts @@ -1,13 +1,23 @@ import type {ErrorObject, ValidateFunction} from 'ajv'; import type {FieldValidator} from 'final-form'; -import type {SetValidationCacheMutator, SetValidationWaitersMutator} from '../mutators'; +import type {ARRAY_AND_OBJECT_ERRORS} from '../constants'; import type { + ErrorsState, + SetValidationCacheMutator, + SetValidationWaitersMutator, + ValidationState, + ValidationWaiter, +} from '../mutators'; +import type { + AsyncValidateError, ErrorMessages, FieldValue, JsonSchema, + ObjectValue, SchemaRendererConfig, SchemaToValueType, + SyncValidateError, Validator, } from '../types'; @@ -31,13 +41,75 @@ export interface GetAjvValidateReturn extends ValidateFunction { export interface GetAjvErrorMessageParams { ajvErrorMessage?: string; - errorMessages?: ErrorMessages; + errorMessages: ErrorMessages | undefined; instancePath: string; keyword: string; mainSchema: JsonSchema; schemaPath: string; } +export interface ValidateErrorItem { + error: SyncValidateError; + path: string[]; +} + +export interface ProcessEntityParametersErrorParams { + allValues: ObjectValue; + error: EntityParametersError; + headName: string; + onAsyncError: (waiter: { + instancePath: string; + params: EntityParametersError['params']; + promise: AsyncValidateError; + }) => void; + onError: (error: ValidateErrorItem) => void; + validationState: ValidationState | undefined; +} + +export interface ProcessAjvErrorParams { + error: ErrorObject; + errorMessages: ErrorMessages | undefined; + headName: string; + mainSchema: Schema; + onError: (error: ValidateErrorItem) => void; +} + +export interface ProcessAjvValidateErrorsParams { + ajvValidateErrors: (ErrorObject | EntityParametersError)[]; + allValues: ObjectValue; + errorMessages: ErrorMessages | undefined; + headName: string; + mainSchema: Schema; + serviceFieldName: string; + setValidationCache: SetValidationCacheMutator; + validationState: ValidationState | undefined; +} + +export interface ProcessAjvValidateErrorsReturn { + ajvErrorItems: ValidateErrorItem[]; + entityParametersErrorItems: ValidateErrorItem[]; + waiters: Record; +} + +export interface ProcessErrorsStateParams { + errorsState: ErrorsState | undefined; +} + +export interface ProcessErrorsStateReturn { + externalPriorityErrorItems: ValidateErrorItem[]; + externalRegularErrorItems: ValidateErrorItem[]; +} + +export interface ProcessErrorItemsParams { + errorItems: ValidateErrorItem[]; + headName: string; + mainSchema: Schema; +} + +export type ProcessErrorItemsReturn = { + [ARRAY_AND_OBJECT_ERRORS]: {[key: string]: boolean | string | undefined}; +} & {[key: string]: SyncValidateError}; + export type GetValidateParams = { config: SchemaRendererConfig; errorMessages?: ErrorMessages; diff --git a/src/lib/unstable/core/SchemaRendererServiceField/utils.ts b/src/lib/unstable/core/SchemaRendererServiceField/utils.ts index 569398c9..91b1c75e 100644 --- a/src/lib/unstable/core/SchemaRendererServiceField/utils.ts +++ b/src/lib/unstable/core/SchemaRendererServiceField/utils.ts @@ -1,13 +1,16 @@ import type {ErrorObject, FuncKeywordDefinition, SchemaValidateFunction} from 'ajv'; import Ajv from 'ajv'; import get from 'lodash/get'; +import isBoolean from 'lodash/isBoolean'; import isEqual from 'lodash/isEqual'; +import isObjectLike from 'lodash/isObjectLike'; import isString from 'lodash/isString'; +import isUndefined from 'lodash/isUndefined'; import mapValues from 'lodash/mapValues'; import omit from 'lodash/omit'; import set from 'lodash/set'; -import {ARRAY_AND_OBJECT_ERRORS, EMPTY_OBJECT, JsonSchemaType, OBJECT_ERROR} from '../constants'; +import {ARRAY_AND_OBJECT_ERRORS, EMPTY_OBJECT, JsonSchemaType} from '../constants'; import type {ErrorsState, ValidationState, ValidationWaiter} from '../mutators'; import type {FieldValue, JsonSchema, ObjectValue, SyncValidateError, Validator} from '../types'; import {getSchemaByFinalFormPath, parseFinalFormPath} from '../utils'; @@ -19,13 +22,22 @@ import type { GetAjvValidateReturn, GetValidateParams, GetValidateReturn, + ProcessAjvErrorParams, + ProcessAjvValidateErrorsParams, + ProcessAjvValidateErrorsReturn, + ProcessEntityParametersErrorParams, + ProcessErrorItemsParams, + ProcessErrorItemsReturn, + ProcessErrorsStateParams, + ProcessErrorsStateReturn, + ValidateErrorItem, } from './types'; export const getAjvValidate = ({ config, mainSchema, }: GetAjvValidateParams): GetAjvValidateReturn => { - function entityParametersValidate(_: any, value: FieldValue, schema?: JsonSchema) { + function entityParametersValidate(_: unknown, value: FieldValue, schema?: JsonSchema) { if (schema) { const validatorType: string | undefined = get(schema, 'entityParameters.validatorType'); const validator: Validator | undefined = get( @@ -124,6 +136,62 @@ const getSchemaByInstancePath = ( return mainSchema; }; +const getValuePaths = (value: unknown, path: string[] = []) => { + const result: string[][] = []; + + const isObject = (v: unknown): v is Record => + v !== null && typeof v === 'object' && !Array.isArray(v); + + if (Array.isArray(value)) { + value.forEach((_, index) => { + result.push(...getValuePaths(value[index], [...path, `${index}`])); + }); + } else if (isObject(value)) { + Object.keys(value).forEach((key) => { + result.push(...getValuePaths(get(value, key), [...path, key])); + }); + } else { + result.push(path); + } + + return result; +}; + +const processEntityParametersError = ({ + allValues, + error, + headName, + onAsyncError, + onError, + validationState, +}: ProcessEntityParametersErrorParams) => { + const waiter = validationState?.waiters?.[error.instancePath]; + const cache = validationState?.cache?.[error.instancePath]; + const cacheItem = cache?.find((item) => isEqual(error.params, omit(item, 'result'))); + + if (cacheItem?.result) { + onError({ + error: cacheItem.result, + path: [...parseFinalFormPath(headName), ...parseInstancePath(error.instancePath)], + }); + } else if (!waiter || !isEqual(error.params, waiter)) { + const errorOrPromise = error.params.validator(error.params.value, allValues as ObjectValue); + + if (errorOrPromise instanceof Promise) { + onAsyncError({ + instancePath: error.instancePath, + params: error.params, + promise: errorOrPromise, + }); + } else { + onError({ + error: errorOrPromise, + path: [...parseFinalFormPath(headName), ...parseInstancePath(error.instancePath)], + }); + } + } +}; + const getAjvErrorMessage = ({ ajvErrorMessage, errorMessages = EMPTY_OBJECT, @@ -154,6 +222,152 @@ const getAjvErrorMessage = ({ ); }; +const processAjvError = ({ + error, + errorMessages, + headName, + mainSchema, + onError, +}: ProcessAjvErrorParams) => { + let instancePath = error.instancePath; + let keyword = error.keyword; + let schemaPath = error.schemaPath; + + if (keyword === 'required' || keyword === 'dependencies') { + instancePath += `/${error.params.missingProperty}`; + } else if (keyword === 'if') { + keyword = error.params.failingKeyword; + schemaPath = schemaPath.slice(0, -'if'.length) + error.params.failingKeyword; + } + + onError({ + path: [...parseFinalFormPath(headName), ...parseInstancePath(error.instancePath)], + error: getAjvErrorMessage({ + ajvErrorMessage: error.message, + errorMessages, + instancePath, + keyword, + mainSchema, + schemaPath, + }), + }); +}; + +const processAjvValidateErrors = ({ + ajvValidateErrors, + allValues, + errorMessages, + headName, + mainSchema, + serviceFieldName, + setValidationCache, + validationState, +}: ProcessAjvValidateErrorsParams): ProcessAjvValidateErrorsReturn => { + const waiters: Record = {}; + const ajvErrorItems: ValidateErrorItem[] = []; + const entityParametersErrorItems: ValidateErrorItem[] = []; + + ajvValidateErrors.forEach((ajvOrEntityParametersError) => { + if (ajvOrEntityParametersError.keyword === 'entityParameters') { + processEntityParametersError({ + allValues: allValues, + error: ajvOrEntityParametersError as EntityParametersError, + headName, + onAsyncError: (w) => { + waiters[w.instancePath] = w.params; + + w.promise.then((result) => { + setValidationCache({ + name: serviceFieldName, + cache: { + [w.instancePath]: { + ...w.params, + result, + }, + }, + }); + }); + }, + onError: (err) => entityParametersErrorItems.push(err), + validationState, + }); + } else { + processAjvError({ + error: ajvOrEntityParametersError as ErrorObject, + errorMessages, + headName, + mainSchema, + onError: (err) => ajvErrorItems.push(err), + }); + } + }); + + return {ajvErrorItems, entityParametersErrorItems, waiters}; +}; + +const processErrorsState = ({errorsState}: ProcessErrorsStateParams): ProcessErrorsStateReturn => { + const getErrorItems = (errors: ErrorsState['priorityErrors'] | ErrorsState['regularErrors']) => + Object.values( + mapValues(errors, (value, key) => ({ + path: parseFinalFormPath(key), + error: value, + })), + ); + + return { + externalPriorityErrorItems: getErrorItems(errorsState?.priorityErrors), + externalRegularErrorItems: getErrorItems(errorsState?.regularErrors), + }; +}; + +const processErrorItems = ({ + errorItems, + headName, + mainSchema, +}: ProcessErrorItemsParams): ProcessErrorItemsReturn => { + const result: ProcessErrorItemsReturn = { + [ARRAY_AND_OBJECT_ERRORS]: {}, + }; + + const setError = (path: string[], error: boolean | string | undefined) => { + const itemSchema = getSchemaByFinalFormPath(path, headName, mainSchema); + + if (itemSchema) { + const arrayOrObjectSchema = + itemSchema.type === JsonSchemaType.Array || + itemSchema.type === JsonSchemaType.Object; + + if (arrayOrObjectSchema) { + result[ARRAY_AND_OBJECT_ERRORS][path.join('.')] = error; + } else { + set(result, path, error); + } + } + }; + + errorItems.forEach((item) => { + if (!item.error) { + return; + } + + if (isObjectLike(item.error)) { + getValuePaths(item.error).forEach((path) => { + setError([...item.path, ...path], get(item.error, path)); + }); + + return; + } + + if (isBoolean(item.error) || isString(item.error) || isUndefined(item.error)) { + setError(item.path, item.error); + + return; + } + }); + + return result; +}; + export const getValidate = ({ config, errorMessages, @@ -168,134 +382,33 @@ export const getValidate = ({ return (_value, allValues, meta) => { ajvValidate(get(allValues, headName)); - if (!ajvValidate.errors?.length) { - return false; - } - - const result = {[ARRAY_AND_OBJECT_ERRORS]: {}}; - - const waiters: Record = {}; - const ajvErrors: {path: string[]; error: SyncValidateError}[] = []; - const entityParametersErrors: {path: string[]; error: SyncValidateError}[] = []; - - ajvValidate.errors.forEach((ajvOrEntityParametersError) => { - if (ajvOrEntityParametersError.keyword === 'entityParameters') { - const entityParametersError = ajvOrEntityParametersError as EntityParametersError; - const validationState: ValidationState | undefined = meta?.data; - - const waiter = validationState?.waiters?.[entityParametersError.instancePath]; - const cache = validationState?.cache?.[entityParametersError.instancePath]; - const cacheItem = cache?.find((item) => - isEqual(entityParametersError.params, omit(item, 'result')), - ); - - if (cacheItem?.result) { - entityParametersErrors.push({ - path: [ - ...parseFinalFormPath(headName), - ...parseInstancePath(entityParametersError.instancePath), - ], - error: cacheItem.result, - }); - } else if (!waiter || !isEqual(entityParametersError.params, waiter)) { - const errorOrPromise = entityParametersError.params.validator( - entityParametersError.params.value, - allValues as ObjectValue, - ); - - if (errorOrPromise instanceof Promise) { - waiters[entityParametersError.instancePath] = entityParametersError.params; - - errorOrPromise.then((result) => { - setValidationCache({ - name: serviceFieldName, - cache: { - [entityParametersError.instancePath]: { - ...entityParametersError.params, - result, - }, - }, - }); - }); - } else { - entityParametersErrors.push({ - path: [ - ...parseFinalFormPath(headName), - ...parseInstancePath(entityParametersError.instancePath), - ], - error: errorOrPromise, - }); - } - } - } else { - const ajvError = ajvOrEntityParametersError as ErrorObject; - - let instancePath = ajvError.instancePath; - let keyword = ajvError.keyword; - let schemaPath = ajvError.schemaPath; - - if (keyword === 'required' || keyword === 'dependencies') { - instancePath += `/${ajvError.params.missingProperty}`; - } else if (keyword === 'if') { - keyword = ajvError.params.failingKeyword; - schemaPath = schemaPath.slice(0, -'if'.length) + ajvError.params.failingKeyword; - } - - ajvErrors.push({ - path: [ - ...parseFinalFormPath(headName), - ...parseInstancePath(ajvError.instancePath), - ], - error: getAjvErrorMessage({ - ajvErrorMessage: ajvError.message, - errorMessages, - instancePath, - keyword, - mainSchema, - schemaPath, - }), - }); - } + const {ajvErrorItems, entityParametersErrorItems, waiters} = processAjvValidateErrors({ + ajvValidateErrors: ajvValidate.errors || [], + allValues: allValues as ObjectValue, + errorMessages, + headName, + mainSchema, + serviceFieldName, + setValidationCache, + validationState: meta?.data as ValidationState | undefined, + }); + const {externalPriorityErrorItems, externalRegularErrorItems} = processErrorsState({ + errorsState: meta?.data as ErrorsState | undefined, }); if (Object.keys(waiters).length) { setValidationWaiters({name: serviceFieldName, waiters}); } - const errorsState: ErrorsState | undefined = meta?.data; - const externalRegularErrors: {path: string[]; error: SyncValidateError}[] = Object.values( - mapValues(errorsState?.regularErrors, (value, key) => ({ - path: parseFinalFormPath(key), - error: value, - })), - ); - const externalPriorityErrors: {path: string[]; error: SyncValidateError}[] = Object.values( - mapValues(errorsState?.priorityErrors, (value, key) => ({ - path: parseFinalFormPath(key), - error: value, - })), - ); - - [ - ...externalRegularErrors, - ...ajvErrors, - ...entityParametersErrors, - ...externalPriorityErrors, - ].forEach((item) => { - if (item.error) { - const itemSchema = getSchemaByFinalFormPath(item.path, headName, mainSchema); - - if (itemSchema) { - const arraySchema = itemSchema.type === JsonSchemaType.Array; - const objectSchema = itemSchema.type === JsonSchemaType.Object; - - set( - arraySchema || objectSchema ? result[ARRAY_AND_OBJECT_ERRORS] : result, - objectSchema ? [...item.path, OBJECT_ERROR] : item.path, - item.error, - ); - } - } + const result = processErrorItems({ + errorItems: [ + ...externalRegularErrorItems, + ...ajvErrorItems, + ...entityParametersErrorItems, + ...externalPriorityErrorItems, + ], + headName, + mainSchema, }); return result; diff --git a/src/lib/unstable/core/constants.ts b/src/lib/unstable/core/constants.ts index 4f81d787..7d3da4bc 100644 --- a/src/lib/unstable/core/constants.ts +++ b/src/lib/unstable/core/constants.ts @@ -14,5 +14,3 @@ export enum JsonSchemaType { export const EMPTY_OBJECT = {}; export const ARRAY_AND_OBJECT_ERRORS = 'ARRAY_AND_OBJECT_ERRORS'; - -export const OBJECT_ERROR = 'OBJECT_ERROR'; diff --git a/src/lib/unstable/core/mutators/set-errors/types.ts b/src/lib/unstable/core/mutators/set-errors/types.ts index df96a757..1f4d7e49 100644 --- a/src/lib/unstable/core/mutators/set-errors/types.ts +++ b/src/lib/unstable/core/mutators/set-errors/types.ts @@ -1,21 +1,23 @@ import type {MutableState, Tools} from 'final-form'; +import type {SyncValidateError} from '../../types'; + export interface ErrorsState { priorityErrors?: { - [key: string]: string | undefined; + [key: string]: SyncValidateError; }; regularErrors?: { - [key: string]: string | undefined; + [key: string]: SyncValidateError; }; } export interface SetErrorsParams { priorityErrors?: { - [key: string]: string | undefined; + [key: string]: SyncValidateError; }; serviceFieldName: string; regularErrors?: { - [key: string]: string | undefined; + [key: string]: SyncValidateError; }; } diff --git a/src/lib/unstable/core/types/validation.ts b/src/lib/unstable/core/types/validation.ts index 55b0d0d2..d21c7a55 100644 --- a/src/lib/unstable/core/types/validation.ts +++ b/src/lib/unstable/core/types/validation.ts @@ -2,11 +2,10 @@ import type {SchemaToValueType} from './helpers'; import type {JsonSchema} from './schema'; import type {ObjectValue} from './values'; -export type SyncValidateError = - | boolean - | string - | Record - | undefined; +type ArrayError = SyncValidateError[]; +interface ObjectError extends Record {} + +export type SyncValidateError = ArrayError | boolean | string | ObjectError | undefined; export type AsyncValidateError = Promise; diff --git a/src/lib/unstable/core/useSchemaRendererField/useSchemaRendererField.ts b/src/lib/unstable/core/useSchemaRendererField/useSchemaRendererField.ts index 25845fe3..b1087fb1 100644 --- a/src/lib/unstable/core/useSchemaRendererField/useSchemaRendererField.ts +++ b/src/lib/unstable/core/useSchemaRendererField/useSchemaRendererField.ts @@ -4,8 +4,8 @@ import get from 'lodash/get'; import {type FieldRenderProps, type UseFieldConfig, useField} from 'react-final-form'; import {SchemaRendererContext} from '../SchemaRendererContext'; -import {ARRAY_AND_OBJECT_ERRORS, JsonSchemaType, OBJECT_ERROR} from '../constants'; -import {getSchemaByFinalFormPath, parseFinalFormPath} from '../utils'; +import {ARRAY_AND_OBJECT_ERRORS, JsonSchemaType} from '../constants'; +import {getSchemaByFinalFormPath} from '../utils'; export const useSchemaRendererField = < FieldValue = any, @@ -24,21 +24,15 @@ export const useSchemaRendererField = < const field = useField(name, config); const serviceField = useField(serviceFieldName, {subscription: {error: true}}); - const {arraySchema, objectSchema} = React.useMemo(() => { + const arrayOrObjectSchema = React.useMemo(() => { const type = getSchemaByFinalFormPath(name, headName, mainSchema)?.type; - return { - arraySchema: type === JsonSchemaType.Array, - objectSchema: type === JsonSchemaType.Object, - }; + return type === JsonSchemaType.Array || type === JsonSchemaType.Object; }, [headName, name, mainSchema]); - const error = get( - arraySchema || objectSchema - ? get(serviceField.meta.error, ARRAY_AND_OBJECT_ERRORS) - : serviceField.meta.error, - objectSchema ? [...parseFinalFormPath(name), OBJECT_ERROR] : name, - ); + const error = arrayOrObjectSchema + ? get(serviceField.meta.error, [ARRAY_AND_OBJECT_ERRORS, name]) + : get(serviceField.meta.error, name); return error ? {...field, meta: {...field.meta, error}} : field; }; diff --git a/src/lib/unstable/kit/Text.tsx b/src/lib/unstable/kit/Text.tsx index e8ff74b0..8d712712 100644 --- a/src/lib/unstable/kit/Text.tsx +++ b/src/lib/unstable/kit/Text.tsx @@ -49,6 +49,13 @@ const Component = < setErrors({ priorityErrors: { [input.name]: 'priorityError', + 'qwe.test.jajaja': { + numberMaximum: 'priorityError', + objectPropertyNames: { + stringEnum: 'priorityError', + }, + }, + 'qwe.test.jajaja.objectPropertyNames': 'priorityError', }, }); } else {