diff --git a/.vscode/settings.json b/.vscode/settings.json index eaf6a64d..eb5a19a1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,7 +5,7 @@ "typescript", "typescriptreact" ], - "prettier.configPath": "./.prettierrc.js", + "prettier.configPath": "./prettier.config.cjs", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": true diff --git a/ajv/src/ajv.ts b/ajv/src/ajv.ts index b3de2c61..7d4d20c9 100644 --- a/ajv/src/ajv.ts +++ b/ajv/src/ajv.ts @@ -1,4 +1,4 @@ -import { toNestError, validateFieldsNatively } from '@hookform/resolvers'; +import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers'; import Ajv, { DefinedError } from 'ajv'; import ajvErrors from 'ajv-errors'; import { appendErrors, FieldError } from 'react-hook-form'; @@ -76,7 +76,7 @@ export const ajvResolver: Resolver = ? { values, errors: {} } : { values: {}, - errors: toNestError( + errors: toNestErrors( parseErrorSchema( validate.errors as DefinedError[], !options.shouldUseNativeValidation && diff --git a/arktype/src/arktype.ts b/arktype/src/arktype.ts index 4bba328d..d42f0025 100644 --- a/arktype/src/arktype.ts +++ b/arktype/src/arktype.ts @@ -1,5 +1,5 @@ import { FieldError, FieldErrors } from 'react-hook-form'; -import { toNestError, validateFieldsNatively } from '@hookform/resolvers'; +import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers'; import type { Resolver } from './types'; import { Problems } from 'arktype'; @@ -28,7 +28,7 @@ export const arktypeResolver: Resolver = if (result.problems) { return { values: {}, - errors: toNestError(parseErrorSchema(result.problems), options), + errors: toNestErrors(parseErrorSchema(result.problems), options), }; } diff --git a/class-validator/src/class-validator.ts b/class-validator/src/class-validator.ts index 4f9464b6..cd103707 100644 --- a/class-validator/src/class-validator.ts +++ b/class-validator/src/class-validator.ts @@ -1,5 +1,5 @@ import { FieldErrors } from 'react-hook-form'; -import { toNestError, validateFieldsNatively } from '@hookform/resolvers'; +import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers'; import { plainToClass } from 'class-transformer'; import { validate, validateSync, ValidationError } from 'class-validator'; import type { Resolver } from './types'; @@ -47,7 +47,7 @@ export const classValidatorResolver: Resolver = if (rawErrors.length) { return { values: {}, - errors: toNestError( + errors: toNestErrors( parseErrors( rawErrors, !options.shouldUseNativeValidation && diff --git a/computed-types/src/computed-types.ts b/computed-types/src/computed-types.ts index f84b48ad..1cf1306a 100644 --- a/computed-types/src/computed-types.ts +++ b/computed-types/src/computed-types.ts @@ -1,5 +1,5 @@ import type { FieldErrors } from 'react-hook-form'; -import { toNestError, validateFieldsNatively } from '@hookform/resolvers'; +import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers'; import type { Resolver } from './types'; import type { ValidationError } from 'computed-types'; @@ -33,7 +33,7 @@ export const computedTypesResolver: Resolver = if (isValidationError(error)) { return { values: {}, - errors: toNestError(parseErrorSchema(error), options), + errors: toNestErrors(parseErrorSchema(error), options), }; } diff --git a/io-ts/src/io-ts.ts b/io-ts/src/io-ts.ts index 8abb0d91..c03c1e05 100644 --- a/io-ts/src/io-ts.ts +++ b/io-ts/src/io-ts.ts @@ -1,6 +1,6 @@ import * as Either from 'fp-ts/Either'; import { pipe } from 'fp-ts/function'; -import { toNestError, validateFieldsNatively } from '@hookform/resolvers'; +import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers'; import errorsToRecord from './errorsToRecord'; import { Resolver } from './types'; @@ -13,7 +13,7 @@ export const ioTsResolver: Resolver = (codec) => (values, _context, options) => !options.shouldUseNativeValidation && options.criteriaMode === 'all', ), ), - Either.mapLeft((errors) => toNestError(errors, options)), + Either.mapLeft((errors) => toNestErrors(errors, options)), Either.fold( (errors) => ({ values: {}, diff --git a/joi/src/joi.ts b/joi/src/joi.ts index 29f69380..5b4baf4e 100644 --- a/joi/src/joi.ts +++ b/joi/src/joi.ts @@ -1,5 +1,5 @@ import { appendErrors, FieldError } from 'react-hook-form'; -import { toNestError, validateFieldsNatively } from '@hookform/resolvers'; +import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers'; import type { ValidationError } from 'joi'; import { Resolver } from './types'; @@ -61,7 +61,7 @@ export const joiResolver: Resolver = if (result.error) { return { values: {}, - errors: toNestError( + errors: toNestErrors( parseErrorSchema( result.error, !options.shouldUseNativeValidation && diff --git a/nope/src/nope.ts b/nope/src/nope.ts index c212e6cc..dc9b5bc4 100644 --- a/nope/src/nope.ts +++ b/nope/src/nope.ts @@ -1,5 +1,5 @@ -import type {FieldError, FieldErrors} from 'react-hook-form'; -import { toNestError, validateFieldsNatively } from '@hookform/resolvers'; +import type { FieldError, FieldErrors } from 'react-hook-form'; +import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers'; import type { ShapeErrors } from 'nope-validator/lib/cjs/types'; import type { Resolver } from './types'; @@ -37,7 +37,7 @@ export const nopeResolver: Resolver = | undefined; if (result) { - return { values: {}, errors: toNestError(parseErrors(result), options) }; + return { values: {}, errors: toNestErrors(parseErrors(result), options) }; } options.shouldUseNativeValidation && validateFieldsNatively({}, options); diff --git a/package.json b/package.json index 1d89e3c4..0f97fa75 100644 --- a/package.json +++ b/package.json @@ -209,10 +209,10 @@ "devDependencies": { "@sinclair/typebox": "^0.31.1", "@testing-library/dom": "^9.3.1", - "@testing-library/jest-dom": "^6.0.0", + "@testing-library/jest-dom": "^6.0.1", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.4.3", - "@types/node": "^20.5.0", + "@types/node": "^20.5.1", "@types/react": "^18.2.20", "@typescript-eslint/eslint-plugin": "^6.4.0", "@typescript-eslint/parser": "^6.4.0", @@ -251,9 +251,9 @@ "vest": "^4.6.11", "vite": "^4.4.9", "vite-tsconfig-paths": "^4.2.0", - "vitest": "^0.34.1", + "vitest": "^0.34.2", "yup": "^1.2.0", - "zod": "^3.22.1" + "zod": "^3.22.2" }, "peerDependencies": { "react-hook-form": "^7.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a23c389..1dedec4b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ devDependencies: specifier: ^9.3.1 version: 9.3.1 '@testing-library/jest-dom': - specifier: ^6.0.0 - version: 6.0.0(vitest@0.34.1) + specifier: ^6.0.1 + version: 6.0.1(vitest@0.34.2) '@testing-library/react': specifier: ^14.0.0 version: 14.0.0(react-dom@18.2.0)(react@18.2.0) @@ -21,8 +21,8 @@ devDependencies: specifier: ^14.4.3 version: 14.4.3(@testing-library/dom@9.3.1) '@types/node': - specifier: ^20.5.0 - version: 20.5.0 + specifier: ^20.5.1 + version: 20.5.1 '@types/react': specifier: ^18.2.20 version: 18.2.20 @@ -133,19 +133,19 @@ devDependencies: version: 4.6.11 vite: specifier: ^4.4.9 - version: 4.4.9(@types/node@20.5.0) + version: 4.4.9(@types/node@20.5.1) vite-tsconfig-paths: specifier: ^4.2.0 version: 4.2.0(typescript@5.1.6)(vite@4.4.9) vitest: - specifier: ^0.34.1 - version: 0.34.1(jsdom@22.1.0) + specifier: ^0.34.2 + version: 0.34.2(jsdom@22.1.0) yup: specifier: ^1.2.0 version: 1.2.0 zod: - specifier: ^3.22.1 - version: 3.22.1 + specifier: ^3.22.2 + version: 3.22.2 packages: @@ -1915,8 +1915,8 @@ packages: pretty-format: 27.5.1 dev: true - /@testing-library/jest-dom@6.0.0(vitest@0.34.1): - resolution: {integrity: sha512-Ye2R3+/oM27jir8CzYPmuWdavTaKwNZcu0d22L9pO/vnOYE0wmrtpw79TQJa8H6gV8/i7yd+pLaqeLlA0rTMfg==} + /@testing-library/jest-dom@6.0.1(vitest@0.34.2): + resolution: {integrity: sha512-0hx/AWrJp8EKr8LmC5jrV3Lx8TZySH7McU1Ix2czBPQnLd458CefSEGjZy7w8kaBRA6LhoPkGjoZ3yqSs338IQ==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} peerDependencies: '@jest/globals': '>= 28' @@ -1941,7 +1941,7 @@ packages: dom-accessibility-api: 0.5.16 lodash: 4.17.21 redent: 3.0.0 - vitest: 0.34.1(jsdom@22.1.0) + vitest: 0.34.2(jsdom@22.1.0) dev: true /@testing-library/react@14.0.0(react-dom@18.2.0)(react@18.2.0): @@ -2003,8 +2003,8 @@ packages: resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} dev: true - /@types/node@20.5.0: - resolution: {integrity: sha512-Mgq7eCtoTjT89FqNoTzzXg2XvCi5VMhRV6+I2aYanc6kQCBImeNaAYRs/DyoVqk1YEUJK5gN9VO7HRIdz4Wo3Q==} + /@types/node@20.5.1: + resolution: {integrity: sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg==} dev: true /@types/parse-json@4.0.0: @@ -2032,7 +2032,7 @@ packages: /@types/resolve@1.17.1: resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} dependencies: - '@types/node': 20.5.0 + '@types/node': 20.5.1 dev: true /@types/scheduler@0.16.3: @@ -2188,43 +2188,43 @@ packages: '@babel/plugin-transform-react-jsx-self': 7.22.5(@babel/core@7.22.10) '@babel/plugin-transform-react-jsx-source': 7.22.5(@babel/core@7.22.10) react-refresh: 0.14.0 - vite: 4.4.9(@types/node@20.5.0) + vite: 4.4.9(@types/node@20.5.1) transitivePeerDependencies: - supports-color dev: true - /@vitest/expect@0.34.1: - resolution: {integrity: sha512-q2CD8+XIsQ+tHwypnoCk8Mnv5e6afLFvinVGCq3/BOT4kQdVQmY6rRfyKkwcg635lbliLPqbunXZr+L1ssUWiQ==} + /@vitest/expect@0.34.2: + resolution: {integrity: sha512-EZm2dMNlLyIfDMha17QHSQcg2KjeAZaXd65fpPzXY5bvnfx10Lcaz3N55uEe8PhF+w4pw+hmrlHLLlRn9vkBJg==} dependencies: - '@vitest/spy': 0.34.1 - '@vitest/utils': 0.34.1 + '@vitest/spy': 0.34.2 + '@vitest/utils': 0.34.2 chai: 4.3.7 dev: true - /@vitest/runner@0.34.1: - resolution: {integrity: sha512-YfQMpYzDsYB7yqgmlxZ06NI4LurHWfrH7Wy3Pvf/z/vwUSgq1zLAb1lWcItCzQG+NVox+VvzlKQrYEXb47645g==} + /@vitest/runner@0.34.2: + resolution: {integrity: sha512-8ydGPACVX5tK3Dl0SUwxfdg02h+togDNeQX3iXVFYgzF5odxvaou7HnquALFZkyVuYskoaHUOqOyOLpOEj5XTA==} dependencies: - '@vitest/utils': 0.34.1 + '@vitest/utils': 0.34.2 p-limit: 4.0.0 pathe: 1.1.1 dev: true - /@vitest/snapshot@0.34.1: - resolution: {integrity: sha512-0O9LfLU0114OqdF8lENlrLsnn024Tb1CsS9UwG0YMWY2oGTQfPtkW+B/7ieyv0X9R2Oijhi3caB1xgGgEgclSQ==} + /@vitest/snapshot@0.34.2: + resolution: {integrity: sha512-qhQ+xy3u4mwwLxltS4Pd4SR+XHv4EajiTPNY3jkIBLUApE6/ce72neJPSUQZ7bL3EBuKI+NhvzhGj3n5baRQUQ==} dependencies: magic-string: 0.30.2 pathe: 1.1.1 pretty-format: 29.6.2 dev: true - /@vitest/spy@0.34.1: - resolution: {integrity: sha512-UT4WcI3EAPUNO8n6y9QoEqynGGEPmmRxC+cLzneFFXpmacivjHZsNbiKD88KUScv5DCHVDgdBsLD7O7s1enFcQ==} + /@vitest/spy@0.34.2: + resolution: {integrity: sha512-yd4L9OhfH6l0Av7iK3sPb3MykhtcRN5c5K5vm1nTbuN7gYn+yvUVVsyvzpHrjqS7EWqn9WsPJb7+0c3iuY60tA==} dependencies: tinyspy: 2.1.1 dev: true - /@vitest/utils@0.34.1: - resolution: {integrity: sha512-/ql9dsFi4iuEbiNcjNHQWXBum7aL8pyhxvfnD9gNtbjR9fUKAjxhj4AA3yfLXg6gJpMGGecvtF8Au2G9y3q47Q==} + /@vitest/utils@0.34.2: + resolution: {integrity: sha512-Lzw+kAsTPubhoQDp1uVAOP6DhNia1GMDsI9jgB0yMn+/nDaPieYQ88lKqz/gGjSHL4zwOItvpehec9OY+rS73w==} dependencies: diff-sequences: 29.4.3 loupe: 2.3.6 @@ -4062,7 +4062,7 @@ packages: resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 20.5.0 + '@types/node': 20.5.1 merge-stream: 2.0.0 supports-color: 7.2.0 dev: true @@ -5784,8 +5784,8 @@ packages: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} dev: true - /std-env@3.3.3: - resolution: {integrity: sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==} + /std-env@3.4.0: + resolution: {integrity: sha512-YqHeQIIQ8r1VtUZOTOyjsAXAsjr369SplZ5rlQaiJTBsvodvPSCME7vuz8pnQltbQ0Cw0lyFo5Q8uyNwYQ58Xw==} dev: true /stop-iteration-iterator@1.0.0: @@ -6269,8 +6269,8 @@ packages: vest-utils: 0.1.1 dev: true - /vite-node@0.34.1(@types/node@20.5.0): - resolution: {integrity: sha512-odAZAL9xFMuAg8aWd7nSPT+hU8u2r9gU3LRm9QKjxBEF2rRdWpMuqkrkjvyVQEdNFiBctqr2Gg4uJYizm5Le6w==} + /vite-node@0.34.2(@types/node@20.5.1): + resolution: {integrity: sha512-JtW249Zm3FB+F7pQfH56uWSdlltCo1IOkZW5oHBzeQo0iX4jtC7o1t9aILMGd9kVekXBP2lfJBEQt9rBh07ebA==} engines: {node: '>=v14.18.0'} hasBin: true dependencies: @@ -6279,7 +6279,7 @@ packages: mlly: 1.4.0 pathe: 1.1.1 picocolors: 1.0.0 - vite: 4.4.9(@types/node@20.5.0) + vite: 4.4.9(@types/node@20.5.1) transitivePeerDependencies: - '@types/node' - less @@ -6302,13 +6302,13 @@ packages: debug: 4.3.4 globrex: 0.1.2 tsconfck: 2.1.2(typescript@5.1.6) - vite: 4.4.9(@types/node@20.5.0) + vite: 4.4.9(@types/node@20.5.1) transitivePeerDependencies: - supports-color - typescript dev: true - /vite@4.4.9(@types/node@20.5.0): + /vite@4.4.9(@types/node@20.5.1): resolution: {integrity: sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true @@ -6336,7 +6336,7 @@ packages: terser: optional: true dependencies: - '@types/node': 20.5.0 + '@types/node': 20.5.1 esbuild: 0.18.20 postcss: 8.4.28 rollup: 3.28.0 @@ -6344,8 +6344,8 @@ packages: fsevents: 2.3.2 dev: true - /vitest@0.34.1(jsdom@22.1.0): - resolution: {integrity: sha512-G1PzuBEq9A75XSU88yO5G4vPT20UovbC/2osB2KEuV/FisSIIsw7m5y2xMdB7RsAGHAfg2lPmp2qKr3KWliVlQ==} + /vitest@0.34.2(jsdom@22.1.0): + resolution: {integrity: sha512-WgaIvBbjsSYMq/oiMlXUI7KflELmzM43BEvkdC/8b5CAod4ryAiY2z8uR6Crbi5Pjnu5oOmhKa9sy7uk6paBxQ==} engines: {node: '>=v14.18.0'} hasBin: true peerDependencies: @@ -6377,12 +6377,12 @@ packages: dependencies: '@types/chai': 4.3.5 '@types/chai-subset': 1.3.3 - '@types/node': 20.5.0 - '@vitest/expect': 0.34.1 - '@vitest/runner': 0.34.1 - '@vitest/snapshot': 0.34.1 - '@vitest/spy': 0.34.1 - '@vitest/utils': 0.34.1 + '@types/node': 20.5.1 + '@vitest/expect': 0.34.2 + '@vitest/runner': 0.34.2 + '@vitest/snapshot': 0.34.2 + '@vitest/spy': 0.34.2 + '@vitest/utils': 0.34.2 acorn: 8.10.0 acorn-walk: 8.2.0 cac: 6.7.14 @@ -6393,12 +6393,12 @@ packages: magic-string: 0.30.2 pathe: 1.1.1 picocolors: 1.0.0 - std-env: 3.3.3 + std-env: 3.4.0 strip-literal: 1.3.0 tinybench: 2.5.0 tinypool: 0.7.0 - vite: 4.4.9(@types/node@20.5.0) - vite-node: 0.34.1(@types/node@20.5.0) + vite: 4.4.9(@types/node@20.5.1) + vite-node: 0.34.2(@types/node@20.5.1) why-is-node-running: 2.2.2 transitivePeerDependencies: - less @@ -6600,6 +6600,6 @@ packages: type-fest: 2.19.0 dev: true - /zod@3.22.1: - resolution: {integrity: sha512-+qUhAMl414+Elh+fRNtpU+byrwjDFOS1N7NioLY+tSlcADTx4TkCUua/hxJvxwDXcV4397/nZ420jy4n4+3WUg==} + /zod@3.22.2: + resolution: {integrity: sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==} dev: true diff --git a/.prettierrc.js b/prettier.config.cjs similarity index 100% rename from .prettierrc.js rename to prettier.config.cjs diff --git a/src/__tests__/__snapshots__/toNestObject.ts.snap b/src/__tests__/__snapshots__/toNestError.ts.snap similarity index 100% rename from src/__tests__/__snapshots__/toNestObject.ts.snap rename to src/__tests__/__snapshots__/toNestError.ts.snap diff --git a/src/__tests__/__snapshots__/toNestErrors.ts.snap b/src/__tests__/__snapshots__/toNestErrors.ts.snap new file mode 100644 index 00000000..d58e0527 --- /dev/null +++ b/src/__tests__/__snapshots__/toNestErrors.ts.snap @@ -0,0 +1,67 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`transforms flat object to nested object 1`] = ` +{ + "name": { + "message": "first message", + "ref": { + "reportValidity": [MockFunction spy], + "setCustomValidity": [MockFunction spy], + }, + "type": "st", + }, + "test": [ + { + "name": { + "message": "second message", + "ref": undefined, + "type": "nd", + }, + }, + ], +} +`; + +exports[`transforms flat object to nested object and shouldUseNativeValidation: true 1`] = ` +{ + "name": { + "message": "first message", + "ref": { + "reportValidity": [MockFunction spy] { + "calls": [ + [], + ], + "results": [ + { + "type": "return", + "value": undefined, + }, + ], + }, + "setCustomValidity": [MockFunction spy] { + "calls": [ + [ + "first message", + ], + ], + "results": [ + { + "type": "return", + "value": undefined, + }, + ], + }, + }, + "type": "st", + }, + "test": [ + { + "name": { + "message": "second message", + "ref": undefined, + "type": "nd", + }, + }, + ], +} +`; diff --git a/src/__tests__/toNestErrors.ts b/src/__tests__/toNestErrors.ts new file mode 100644 index 00000000..6ac0df66 --- /dev/null +++ b/src/__tests__/toNestErrors.ts @@ -0,0 +1,206 @@ +import { Field, FieldError, InternalFieldName } from 'react-hook-form'; +import { toNestErrors } from '../toNestErrors'; + +const flatObject: Record = { + name: { type: 'st', message: 'first message' }, + 'test.0.name': { type: 'nd', message: 'second message' }, +}; + +const fields = { + name: { + ref: { + reportValidity: vi.fn(), + setCustomValidity: vi.fn(), + }, + }, + unused: { + ref: { name: 'unusedRef' }, + }, +} as any as Record; + +test('transforms flat object to nested object', () => { + expect( + toNestErrors(flatObject, { fields, shouldUseNativeValidation: false }), + ).toMatchSnapshot(); +}); + +test('transforms flat object to nested object and shouldUseNativeValidation: true', () => { + expect( + toNestErrors(flatObject, { fields, shouldUseNativeValidation: true }), + ).toMatchSnapshot(); + expect( + (fields.name.ref as HTMLInputElement).reportValidity, + ).toHaveBeenCalledTimes(1); + expect( + (fields.name.ref as HTMLInputElement).setCustomValidity, + ).toHaveBeenCalledTimes(1); + expect( + (fields.name.ref as HTMLInputElement).setCustomValidity, + ).toHaveBeenCalledWith(flatObject.name.message); +}); + +test('transforms flat object to nested object with root error for field array', () => { + const result = toNestErrors( + { + username: { type: 'username', message: 'username is required' }, + 'fieldArrayWithRootError.0.name': { + type: 'first', + message: 'first message', + }, + 'fieldArrayWithRootError.0.nestFieldArrayWithoutRootError.0.title': { + type: 'title', + message: 'title', + }, + 'fieldArrayWithRootError.0.nestFieldArrayWithRootError': { + type: 'nested-root-title', + message: 'nested root errors', + }, + 'fieldArrayWithRootError.0.nestFieldArrayWithRootError.0.title': { + type: 'nestFieldArrayWithRootError-title', + message: 'nestFieldArrayWithRootError-title', + }, + 'fieldArrayWithRootError.1.name': { + type: 'second', + message: 'second message', + }, + fieldArrayWithRootError: { type: 'root-error', message: 'root message' }, + 'fieldArrayWithoutRootError.0.name': { + type: 'first', + message: 'first message', + }, + 'fieldArrayWithoutRootError.1.name': { + type: 'second', + message: 'second message', + }, + }, + { + fields: { + username: { name: 'username', ref: { name: 'username' } }, + fieldArrayWithRootError: { + name: 'fieldArrayWithRootError', + ref: { name: 'fieldArrayWithRootError' }, + }, + 'fieldArrayWithRootError.0.name': { + name: 'fieldArrayWithRootError.0.name', + ref: { name: 'fieldArrayWithRootError.0.name' }, + }, + 'fieldArrayWithRootError.0.nestFieldArrayWithoutRootError.0.title': { + name: 'fieldArrayWithRootError.0.nestFieldArrayWithoutRootError.0.title', + ref: { + name: 'fieldArrayWithRootError.0.nestFieldArrayWithoutRootError.0.title', + }, + }, + 'fieldArrayWithRootError.0.nestFieldArrayWithRootError': { + name: 'fieldArrayWithRootError.0.nestFieldArrayWithRootError', + ref: { + name: 'fieldArrayWithRootError.0.nestFieldArrayWithRootError', + }, + }, + 'fieldArrayWithRootError.0.nestFieldArrayWithRootError.0.title': { + name: 'fieldArrayWithRootError.0.nestFieldArrayWithRootError.0.title', + ref: { + name: 'fieldArrayWithRootError.0.nestFieldArrayWithRootError.0.title', + }, + }, + 'fieldArrayWithRootError.1.name': { + name: 'fieldArrayWithRootError.1.name', + ref: { name: 'fieldArrayWithRootError.1.name' }, + }, + 'fieldArrayWithoutRootError.0.name': { + name: 'fieldArrayWithoutRootError.0.name', + ref: { name: 'fieldArrayWithoutRootError.0.name' }, + }, + 'fieldArrayWithoutRootError.1.name': { + name: 'fieldArrayWithoutRootError.1.name', + ref: { name: 'fieldArrayWithoutRootError.1.name' }, + }, + }, + names: [ + 'username', + 'fieldArrayWithRootError', + 'fieldArrayWithRootError.0.name', + 'fieldArrayWithRootError.0.nestFieldArrayWithoutRootError.0.title', + 'fieldArrayWithRootError.1.name', + 'fieldArrayWithoutRootError.0.name', + 'fieldArrayWithoutRootError.1.name', + 'fieldArrayWithRootError.0.nestFieldArrayWithRootError', + 'fieldArrayWithRootError.0.nestFieldArrayWithRootError.0.title', + ], + shouldUseNativeValidation: false, + }, + ); + + expect(result).toEqual({ + username: { + type: 'username', + message: 'username is required', + ref: { name: 'username' }, + }, + fieldArrayWithRootError: { + '0': { + name: { + type: 'first', + message: 'first message', + ref: { name: 'fieldArrayWithRootError.0.name' }, + }, + nestFieldArrayWithoutRootError: [ + { + title: { + type: 'title', + message: 'title', + ref: { + name: 'fieldArrayWithRootError.0.nestFieldArrayWithoutRootError.0.title', + }, + }, + }, + ], + nestFieldArrayWithRootError: { + '0': { + title: { + type: 'nestFieldArrayWithRootError-title', + message: 'nestFieldArrayWithRootError-title', + ref: { + name: 'fieldArrayWithRootError.0.nestFieldArrayWithRootError.0.title', + }, + }, + }, + root: { + type: 'nested-root-title', + message: 'nested root errors', + ref: { + name: 'fieldArrayWithRootError.0.nestFieldArrayWithRootError', + }, + }, + }, + }, + '1': { + name: { + type: 'second', + message: 'second message', + ref: { name: 'fieldArrayWithRootError.1.name' }, + }, + }, + root: { + type: 'root-error', + message: 'root message', + ref: { name: 'fieldArrayWithRootError' }, + }, + }, + fieldArrayWithoutRootError: [ + { + name: { + type: 'first', + message: 'first message', + ref: { name: 'fieldArrayWithoutRootError.0.name' }, + }, + }, + { + name: { + type: 'second', + message: 'second message', + ref: { name: 'fieldArrayWithoutRootError.1.name' }, + }, + }, + ], + }); +}); diff --git a/src/__tests__/toNestObject.ts b/src/__tests__/toNestObject.ts deleted file mode 100644 index e416e8b7..00000000 --- a/src/__tests__/toNestObject.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Field, FieldError, InternalFieldName } from 'react-hook-form'; -import { toNestError } from '../toNestError'; - -const flatObject: Record = { - name: { type: 'st', message: 'first message' }, - 'test.0.name': { type: 'nd', message: 'second message' }, -}; - -const fields = { - name: { - ref: { - reportValidity: vi.fn(), - setCustomValidity: vi.fn(), - }, - }, - unused: { - ref: { name: 'unusedRef' }, - }, -} as any as Record; - -test('transforms flat object to nested object', () => { - expect( - toNestError(flatObject, { fields, shouldUseNativeValidation: false }), - ).toMatchSnapshot(); -}); - -test('transforms flat object to nested object and shouldUseNativeValidation: true', () => { - expect( - toNestError(flatObject, { fields, shouldUseNativeValidation: true }), - ).toMatchSnapshot(); - expect( - (fields.name.ref as HTMLInputElement).reportValidity, - ).toHaveBeenCalledTimes(1); - expect( - (fields.name.ref as HTMLInputElement).setCustomValidity, - ).toHaveBeenCalledTimes(1); - expect( - (fields.name.ref as HTMLInputElement).setCustomValidity, - ).toHaveBeenCalledWith(flatObject.name.message); -}); diff --git a/src/index.ts b/src/index.ts index d110470d..cd22ab17 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,2 @@ -export * from './toNestError'; +export * from './toNestErrors'; export * from './validateFieldsNatively'; diff --git a/src/toNestError.ts b/src/toNestError.ts deleted file mode 100644 index 21617695..00000000 --- a/src/toNestError.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { - set, - get, - FieldErrors, - Field, - ResolverOptions, - FieldValues, -} from 'react-hook-form'; -import { validateFieldsNatively } from './validateFieldsNatively'; - -export const toNestError = ( - errors: FieldErrors, - options: ResolverOptions, -): FieldErrors => { - options.shouldUseNativeValidation && validateFieldsNatively(errors, options); - - const fieldErrors = {} as FieldErrors; - for (const path in errors) { - const field = get(options.fields, path) as Field['_f'] | undefined; - - set( - fieldErrors, - path, - Object.assign(errors[path] || {}, { ref: field && field.ref }), - ); - } - - return fieldErrors; -}; diff --git a/src/toNestErrors.ts b/src/toNestErrors.ts new file mode 100644 index 00000000..5c4b839b --- /dev/null +++ b/src/toNestErrors.ts @@ -0,0 +1,47 @@ +import { + set, + get, + FieldErrors, + Field, + ResolverOptions, + FieldValues, + InternalFieldName, +} from 'react-hook-form'; +import { validateFieldsNatively } from './validateFieldsNatively'; + +export const toNestErrors = ( + errors: FieldErrors, + options: ResolverOptions, +): FieldErrors => { + options.shouldUseNativeValidation && validateFieldsNatively(errors, options); + + const fieldErrors = {} as FieldErrors; + for (const path in errors) { + const field = get(options.fields, path) as Field['_f'] | undefined; + const error = Object.assign(errors[path] || {}, { + ref: field && field.ref, + }); + + if (isNameInFieldArray(options.names || Object.keys(errors), path)) { + const fieldArrayErrors = Object.assign( + {}, + compact(get(fieldErrors, path)), + ); + + set(fieldArrayErrors, 'root', error); + set(fieldErrors, path, fieldArrayErrors); + } else { + set(fieldErrors, path, error); + } + } + + return fieldErrors; +}; + +const compact = (value: TValue[]) => + Array.isArray(value) ? value.filter(Boolean) : []; + +const isNameInFieldArray = ( + names: InternalFieldName[], + name: InternalFieldName, +) => names.some((n) => n.startsWith(name + '.')); diff --git a/superstruct/src/superstruct.ts b/superstruct/src/superstruct.ts index a563d222..9704891f 100644 --- a/superstruct/src/superstruct.ts +++ b/superstruct/src/superstruct.ts @@ -1,5 +1,5 @@ import { FieldError } from 'react-hook-form'; -import { toNestError, validateFieldsNatively } from '@hookform/resolvers'; +import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers'; import { StructError, validate } from 'superstruct'; import { Resolver } from './types'; @@ -22,7 +22,7 @@ export const superstructResolver: Resolver = if (result[0]) { return { values: {}, - errors: toNestError(parseErrorSchema(result[0]), options), + errors: toNestErrors(parseErrorSchema(result[0]), options), }; } diff --git a/typanion/src/typanion.ts b/typanion/src/typanion.ts index c52e6b68..7d716d98 100644 --- a/typanion/src/typanion.ts +++ b/typanion/src/typanion.ts @@ -1,5 +1,5 @@ import type { FieldError, FieldErrors } from 'react-hook-form'; -import { toNestError, validateFieldsNatively } from '@hookform/resolvers'; +import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers'; import type { Resolver } from './types'; const parseErrors = (errors: string[], parsedErrors: FieldErrors = {}) => { @@ -37,5 +37,5 @@ export const typanionResolver: Resolver = return { values, errors: {} }; } - return { values: {}, errors: toNestError(parsedErrors, options) }; + return { values: {}, errors: toNestErrors(parsedErrors, options) }; }; diff --git a/typebox/src/typebox.ts b/typebox/src/typebox.ts index 3424ab37..3fe79ae1 100644 --- a/typebox/src/typebox.ts +++ b/typebox/src/typebox.ts @@ -1,5 +1,5 @@ import { appendErrors, FieldError, FieldErrors } from 'react-hook-form'; -import { toNestError, validateFieldsNatively } from '@hookform/resolvers'; +import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers'; import type { Resolver } from './types'; import { Value, ValueError } from '@sinclair/typebox/value'; @@ -53,7 +53,7 @@ export const typeboxResolver: Resolver = return { values: {}, - errors: toNestError( + errors: toNestErrors( parseErrorSchema( errors, !options.shouldUseNativeValidation && options.criteriaMode === 'all', diff --git a/valibot/src/valibot.ts b/valibot/src/valibot.ts index ec269b13..d57af560 100644 --- a/valibot/src/valibot.ts +++ b/valibot/src/valibot.ts @@ -1,4 +1,4 @@ -import { toNestError } from '@hookform/resolvers'; +import { toNestErrors } from '@hookform/resolvers'; import type { Resolver } from './types'; import { BaseSchema, @@ -13,7 +13,7 @@ const parseErrors = ( validateAllFieldCriteria: boolean, ): FieldErrors => { const errors: Record = {}; - for (; valiErrors.issues.length;) { + for (; valiErrors.issues.length; ) { const error = valiErrors.issues[0]; if (!error.path) { continue; @@ -47,45 +47,45 @@ const parseErrors = ( export const valibotResolver: Resolver = (schema, schemaOptions, resolverOptions = {}) => - async (values, _, options) => { - try { - const schemaOpts = Object.assign( - {}, - { - abortEarly: false, - abortPipeEarly: false, - }, - schemaOptions, - ); + async (values, _, options) => { + try { + const schemaOpts = Object.assign( + {}, + { + abortEarly: false, + abortPipeEarly: false, + }, + schemaOptions, + ); - const parsed = - resolverOptions.mode === 'sync' - ? parse(schema as BaseSchema, values, schemaOpts) - : await parseAsync( + const parsed = + resolverOptions.mode === 'sync' + ? parse(schema as BaseSchema, values, schemaOpts) + : await parseAsync( schema as BaseSchema | BaseSchemaAsync, values, schemaOpts, ); + return { + values: resolverOptions.raw ? values : parsed, + errors: {} as FieldErrors, + }; + } catch (error) { + if (error instanceof ValiError) { return { - values: resolverOptions.raw ? values : parsed, - errors: {} as FieldErrors, - }; - } catch (error) { - if (error instanceof ValiError) { - return { - values: {}, - errors: toNestError( - parseErrors( - error, - !options.shouldUseNativeValidation && + values: {}, + errors: toNestErrors( + parseErrors( + error, + !options.shouldUseNativeValidation && options.criteriaMode === 'all', - ), - options, ), - }; - } - - throw error; + options, + ), + }; } - }; + + throw error; + } + }; diff --git a/vest/src/vest.ts b/vest/src/vest.ts index 5348c1ab..ea750cc7 100644 --- a/vest/src/vest.ts +++ b/vest/src/vest.ts @@ -1,4 +1,4 @@ -import { toNestError, validateFieldsNatively } from '@hookform/resolvers'; +import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers'; import { FieldError } from 'react-hook-form'; import promisify from 'vest/promisify'; import type { VestErrors, Resolver } from './types'; @@ -34,7 +34,7 @@ export const vestResolver: Resolver = if (result.hasErrors()) { return { values: {}, - errors: toNestError( + errors: toNestErrors( parseErrorSchema( result.getErrors(), !options.shouldUseNativeValidation && diff --git a/yup/src/yup.ts b/yup/src/yup.ts index 6536e00c..85ff2ba5 100644 --- a/yup/src/yup.ts +++ b/yup/src/yup.ts @@ -1,5 +1,5 @@ import * as Yup from 'yup'; -import { toNestError, validateFieldsNatively } from '@hookform/resolvers'; +import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers'; import { appendErrors, FieldError, @@ -86,11 +86,11 @@ export function yupResolver( return { values: {}, - errors: toNestError( + errors: toNestErrors( parseErrorSchema( e, !options.shouldUseNativeValidation && - options.criteriaMode === 'all', + options.criteriaMode === 'all', ), options, ), diff --git a/zod/src/zod.ts b/zod/src/zod.ts index 53c95bd1..8a5e2416 100644 --- a/zod/src/zod.ts +++ b/zod/src/zod.ts @@ -1,6 +1,6 @@ import { appendErrors, FieldError, FieldErrors } from 'react-hook-form'; import { z, ZodError } from 'zod'; -import { toNestError, validateFieldsNatively } from '@hookform/resolvers'; +import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers'; import type { Resolver } from './types'; const isZodError = (error: any): error is ZodError => error.errors != null; @@ -73,7 +73,7 @@ export const zodResolver: Resolver = if (isZodError(error)) { return { values: {}, - errors: toNestError( + errors: toNestErrors( parseErrorSchema( error.errors, !options.shouldUseNativeValidation &&