diff --git a/joi/src/__tests__/__fixtures__/data.ts b/joi/src/__tests__/__fixtures__/data.ts new file mode 100644 index 00000000..3404baf0 --- /dev/null +++ b/joi/src/__tests__/__fixtures__/data.ts @@ -0,0 +1,74 @@ +import * as Joi from 'joi'; + +import { Field, InternalFieldName } from 'react-hook-form'; + +export const schema = Joi.object({ + username: Joi.string().alphanum().min(3).max(30).required(), + password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')).required(), + repeatPassword: Joi.ref('password'), + accessToken: [Joi.string(), Joi.number()], + birthYear: Joi.number().integer().min(1900).max(2013), + email: Joi.string().email({ + minDomainSegments: 2, + tlds: { allow: ['com', 'net'] }, + }), + tags: Joi.array().items(Joi.string()).required(), + enabled: Joi.boolean().required(), + like: Joi.array() + .items(Joi.object({ id: Joi.number(), name: Joi.string().length(4) })) + .optional(), +}); + +interface Data { + username: string; + password: string; + repeatPassword: string; + accessToken?: number | string; + birthYear?: number; + email?: string; + tags: string[]; + enabled: boolean; + like: { id: number; name: string }[]; +} + +export const validData: Data = { + username: 'Doe', + password: 'Password123', + repeatPassword: 'Password123', + birthYear: 2000, + email: 'john@doe.com', + tags: ['tag1', 'tag2'], + enabled: true, + like: [ + { + id: 1, + name: 'name', + }, + ], +}; + +export const invalidData = { + password: '___', + email: '', + birthYear: 'birthYear', + like: [{ id: 'z' }], +}; + +export const fields: Record = { + username: { + ref: { name: 'username' }, + name: 'username', + }, + password: { + ref: { name: 'password' }, + name: 'password', + }, + email: { + ref: { name: 'email' }, + name: 'email', + }, + birthday: { + ref: { name: 'birthday' }, + name: 'birthday', + }, +}; diff --git a/joi/src/__tests__/__snapshots__/joi.ts.snap b/joi/src/__tests__/__snapshots__/joi.ts.snap index 8862c0b2..7277cb47 100644 --- a/joi/src/__tests__/__snapshots__/joi.ts.snap +++ b/joi/src/__tests__/__snapshots__/joi.ts.snap @@ -5,26 +5,47 @@ Object { "errors": Object { "birthYear": Object { "message": "\\"birthYear\\" must be a number", + "ref": undefined, "type": "number.base", }, "email": Object { "message": "\\"email\\" is not allowed to be empty", + "ref": Object { + "name": "email", + }, "type": "string.empty", }, "enabled": Object { "message": "\\"enabled\\" is required", + "ref": undefined, "type": "any.required", }, + "like": Array [ + Object { + "id": Object { + "message": "\\"like[0].id\\" must be a number", + "ref": undefined, + "type": "number.base", + }, + }, + ], "password": Object { "message": "\\"password\\" with value \\"___\\" fails to match the required pattern: /^[a-zA-Z0-9]{3,30}$/", + "ref": Object { + "name": "password", + }, "type": "string.pattern.base", }, "tags": Object { "message": "\\"tags\\" is required", + "ref": undefined, "type": "any.required", }, "username": Object { "message": "\\"username\\" is required", + "ref": Object { + "name": "username", + }, "type": "any.required", }, }, @@ -37,26 +58,47 @@ Object { "errors": Object { "birthYear": Object { "message": "\\"birthYear\\" must be a number", + "ref": undefined, "type": "number.base", }, "email": Object { "message": "\\"email\\" is not allowed to be empty", + "ref": Object { + "name": "email", + }, "type": "string.empty", }, "enabled": Object { "message": "\\"enabled\\" is required", + "ref": undefined, "type": "any.required", }, + "like": Array [ + Object { + "id": Object { + "message": "\\"like[0].id\\" must be a number", + "ref": undefined, + "type": "number.base", + }, + }, + ], "password": Object { "message": "\\"password\\" with value \\"___\\" fails to match the required pattern: /^[a-zA-Z0-9]{3,30}$/", + "ref": Object { + "name": "password", + }, "type": "string.pattern.base", }, "tags": Object { "message": "\\"tags\\" is required", + "ref": undefined, "type": "any.required", }, "username": Object { "message": "\\"username\\" is required", + "ref": Object { + "name": "username", + }, "type": "any.required", }, }, @@ -69,6 +111,7 @@ Object { "errors": Object { "birthYear": Object { "message": "\\"birthYear\\" must be a number", + "ref": undefined, "type": "number.base", "types": Object { "number.base": "\\"birthYear\\" must be a number", @@ -76,6 +119,9 @@ Object { }, "email": Object { "message": "\\"email\\" is not allowed to be empty", + "ref": Object { + "name": "email", + }, "type": "string.empty", "types": Object { "string.empty": "\\"email\\" is not allowed to be empty", @@ -83,13 +129,29 @@ Object { }, "enabled": Object { "message": "\\"enabled\\" is required", + "ref": undefined, "type": "any.required", "types": Object { "any.required": "\\"enabled\\" is required", }, }, + "like": Array [ + Object { + "id": Object { + "message": "\\"like[0].id\\" must be a number", + "ref": undefined, + "type": "number.base", + "types": Object { + "number.base": "\\"like[0].id\\" must be a number", + }, + }, + }, + ], "password": Object { "message": "\\"password\\" with value \\"___\\" fails to match the required pattern: /^[a-zA-Z0-9]{3,30}$/", + "ref": Object { + "name": "password", + }, "type": "string.pattern.base", "types": Object { "string.pattern.base": "\\"password\\" with value \\"___\\" fails to match the required pattern: /^[a-zA-Z0-9]{3,30}$/", @@ -97,6 +159,7 @@ Object { }, "tags": Object { "message": "\\"tags\\" is required", + "ref": undefined, "type": "any.required", "types": Object { "any.required": "\\"tags\\" is required", @@ -104,6 +167,9 @@ Object { }, "username": Object { "message": "\\"username\\" is required", + "ref": Object { + "name": "username", + }, "type": "any.required", "types": Object { "any.required": "\\"username\\" is required", @@ -119,6 +185,7 @@ Object { "errors": Object { "birthYear": Object { "message": "\\"birthYear\\" must be a number", + "ref": undefined, "type": "number.base", "types": Object { "number.base": "\\"birthYear\\" must be a number", @@ -126,6 +193,9 @@ Object { }, "email": Object { "message": "\\"email\\" is not allowed to be empty", + "ref": Object { + "name": "email", + }, "type": "string.empty", "types": Object { "string.empty": "\\"email\\" is not allowed to be empty", @@ -133,13 +203,29 @@ Object { }, "enabled": Object { "message": "\\"enabled\\" is required", + "ref": undefined, "type": "any.required", "types": Object { "any.required": "\\"enabled\\" is required", }, }, + "like": Array [ + Object { + "id": Object { + "message": "\\"like[0].id\\" must be a number", + "ref": undefined, + "type": "number.base", + "types": Object { + "number.base": "\\"like[0].id\\" must be a number", + }, + }, + }, + ], "password": Object { "message": "\\"password\\" with value \\"___\\" fails to match the required pattern: /^[a-zA-Z0-9]{3,30}$/", + "ref": Object { + "name": "password", + }, "type": "string.pattern.base", "types": Object { "string.pattern.base": "\\"password\\" with value \\"___\\" fails to match the required pattern: /^[a-zA-Z0-9]{3,30}$/", @@ -147,6 +233,7 @@ Object { }, "tags": Object { "message": "\\"tags\\" is required", + "ref": undefined, "type": "any.required", "types": Object { "any.required": "\\"tags\\" is required", @@ -154,6 +241,9 @@ Object { }, "username": Object { "message": "\\"username\\" is required", + "ref": Object { + "name": "username", + }, "type": "any.required", "types": Object { "any.required": "\\"username\\" is required", diff --git a/joi/src/__tests__/joi.ts b/joi/src/__tests__/joi.ts index c5f808a3..131c649c 100644 --- a/joi/src/__tests__/joi.ts +++ b/joi/src/__tests__/joi.ts @@ -1,105 +1,48 @@ -import * as Joi from 'joi'; import { joiResolver } from '..'; - -const schema = Joi.object({ - username: Joi.string().alphanum().min(3).max(30).required(), - password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')).required(), - repeatPassword: Joi.ref('password'), - accessToken: [Joi.string(), Joi.number()], - birthYear: Joi.number().integer().min(1900).max(2013), - email: Joi.string().email({ - minDomainSegments: 2, - tlds: { allow: ['com', 'net'] }, - }), - tags: Joi.array().items(Joi.string()).required(), - enabled: Joi.boolean().required(), -}); - -interface Data { - username: string; - password: string; - repeatPassword: string; - accessToken?: number | string; - birthYear?: number; - email?: string; - tags: string[]; - enabled: boolean; -} +import { schema, validData, fields, invalidData } from './__fixtures__/data'; describe('joiResolver', () => { it('should return values from joiResolver when validation pass', async () => { - const data: Data = { - username: 'Doe', - password: 'Password123', - repeatPassword: 'Password123', - birthYear: 2000, - email: 'john@doe.com', - tags: ['tag1', 'tag2'], - enabled: true, - }; - const validateAsyncSpy = jest.spyOn(schema, 'validateAsync'); const validateSpy = jest.spyOn(schema, 'validate'); - const result = await joiResolver(schema)(data, undefined, { fields: {} }); + const result = await joiResolver(schema)(validData, undefined, { + fields, + }); expect(validateSpy).not.toHaveBeenCalled(); expect(validateAsyncSpy).toHaveBeenCalledTimes(1); - expect(result).toEqual({ errors: {}, values: data }); + expect(result).toEqual({ errors: {}, values: validData }); }); it('should return values from joiResolver with `mode: sync` when validation pass', async () => { - const data: Data = { - username: 'Doe', - password: 'Password123', - repeatPassword: 'Password123', - birthYear: 2000, - email: 'john@doe.com', - tags: ['tag1', 'tag2'], - enabled: true, - }; - const validateAsyncSpy = jest.spyOn(schema, 'validateAsync'); const validateSpy = jest.spyOn(schema, 'validate'); - const result = await joiResolver(schema, undefined, { mode: 'sync' })( - data, - undefined, - { fields: {} }, - ); + const result = await joiResolver(schema, undefined, { + mode: 'sync', + })(validData, undefined, { fields }); expect(validateAsyncSpy).not.toHaveBeenCalled(); expect(validateSpy).toHaveBeenCalledTimes(1); - expect(result).toEqual({ errors: {}, values: data }); + expect(result).toEqual({ errors: {}, values: validData }); }); it('should return a single error from joiResolver when validation fails', async () => { - const data = { - password: '___', - email: '', - birthYear: 'birthYear', - }; - - const result = await joiResolver(schema)(data, undefined, { fields: {} }); + const result = await joiResolver(schema)(invalidData, undefined, { + fields, + }); expect(result).toMatchSnapshot(); }); it('should return a single error from joiResolver with `mode: sync` when validation fails', async () => { - const data = { - password: '___', - email: '', - birthYear: 'birthYear', - }; - const validateAsyncSpy = jest.spyOn(schema, 'validateAsync'); const validateSpy = jest.spyOn(schema, 'validate'); - const result = await joiResolver(schema, undefined, { mode: 'sync' })( - data, - undefined, - { fields: {} }, - ); + const result = await joiResolver(schema, undefined, { + mode: 'sync', + })(invalidData, undefined, { fields }); expect(validateAsyncSpy).not.toHaveBeenCalled(); expect(validateSpy).toHaveBeenCalledTimes(1); @@ -107,14 +50,8 @@ describe('joiResolver', () => { }); it('should return all the errors from joiResolver when validation fails with `validateAllFieldCriteria` set to true', async () => { - const data = { - password: '___', - email: '', - birthYear: 'birthYear', - }; - - const result = await joiResolver(schema)(data, undefined, { - fields: {}, + const result = await joiResolver(schema)(invalidData, undefined, { + fields, criteriaMode: 'all', }); @@ -122,17 +59,11 @@ describe('joiResolver', () => { }); it('should return all the errors from joiResolver when validation fails with `validateAllFieldCriteria` set to true and `mode: sync`', async () => { - const data = { - password: '___', - email: '', - birthYear: 'birthYear', - }; - const result = await joiResolver(schema, undefined, { mode: 'sync' })( - data, + invalidData, undefined, { - fields: {}, + fields, criteriaMode: 'all', }, ); @@ -141,27 +72,18 @@ describe('joiResolver', () => { }); it('should return values from joiResolver when validation pass and pass down the Joi context', async () => { - const data: Data = { - username: 'Doe', - password: 'Password123', - repeatPassword: 'Password123', - birthYear: 2000, - email: 'john@doe.com', - tags: ['tag1', 'tag2'], - enabled: true, - }; const context = { value: 'context' }; const validateAsyncSpy = jest.spyOn(schema, 'validateAsync'); const validateSpy = jest.spyOn(schema, 'validate'); - const result = await joiResolver(schema)(data, context, { fields: {} }); + const result = await joiResolver(schema)(validData, context, { fields }); expect(validateSpy).not.toHaveBeenCalled(); expect(validateAsyncSpy).toHaveBeenCalledTimes(1); - expect(validateAsyncSpy).toHaveBeenCalledWith(data, { + expect(validateAsyncSpy).toHaveBeenCalledWith(validData, { abortEarly: false, context, }); - expect(result).toEqual({ errors: {}, values: data }); + expect(result).toEqual({ errors: {}, values: validData }); }); }); diff --git a/joi/src/joi.ts b/joi/src/joi.ts index 20b475b8..232944e9 100644 --- a/joi/src/joi.ts +++ b/joi/src/joi.ts @@ -1,5 +1,5 @@ import { appendErrors } from 'react-hook-form'; -import { toNestObject } from '@hookform/resolvers'; +import { toNestError } from '@hookform/resolvers'; import * as Joi from 'joi'; import { convertArrayToPathName } from '@hookform/resolvers'; import { Resolver } from './types'; @@ -50,7 +50,7 @@ export const joiResolver: Resolver = ( abortEarly: false, }, { mode } = { mode: 'async' }, -) => async (values, context, { criteriaMode }) => { +) => async (values, context, { criteriaMode, fields }) => { try { let result; if (mode === 'async') { @@ -78,7 +78,7 @@ export const joiResolver: Resolver = ( } catch (e) { return { values: {}, - errors: toNestObject(parseErrorSchema(e, criteriaMode === 'all')), + errors: toNestError(parseErrorSchema(e, criteriaMode === 'all'), fields), }; } }; diff --git a/src/__tests__/toNestObject.ts b/src/__tests__/toNestObject.ts index bf6eb226..877eaeac 100644 --- a/src/__tests__/toNestObject.ts +++ b/src/__tests__/toNestObject.ts @@ -1,5 +1,5 @@ -import { FieldError } from 'react-hook-form'; -import { toNestObject } from '../toNestObject'; +import { Field, FieldError, InternalFieldName } from 'react-hook-form'; +import { toNestError } from '../toNestError'; test('transforms flat object to nested object', () => { const flatObject: Record = { @@ -8,22 +8,39 @@ test('transforms flat object to nested object', () => { 'n.test': { type: 'rd', message: 'third message' }, }; - expect(toNestObject(flatObject)).toMatchInlineSnapshot(` + const fields = ({ + name: { + ref: 'nameRef', + }, + n: { + test: { + ref: 'testRef', + }, + }, + unused: { + ref: 'unusedRef', + }, + } as any) as Record; + + expect(toNestError(flatObject, fields)).toMatchInlineSnapshot(` Object { "n": Object { "test": Object { "message": "third message", + "ref": "testRef", "type": "rd", }, }, "name": Object { "message": "first message", + "ref": "nameRef", "type": "st", }, "test": Array [ Object { "name": Object { "message": "second message", + "ref": undefined, "type": "nd", }, }, diff --git a/src/index.ts b/src/index.ts index 28e78957..dc3c3311 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,2 @@ export * from './convertArrayToPathName'; -export * from './toNestObject'; +export * from './toNestError'; diff --git a/src/toNestError.ts b/src/toNestError.ts new file mode 100644 index 00000000..72672aed --- /dev/null +++ b/src/toNestError.ts @@ -0,0 +1,25 @@ +import { + set, + get, + FieldError, + FieldErrors, + Field, + InternalFieldName, +} from 'react-hook-form'; + +export const toNestError = ( + errors: Record, + fields: Record, +): FieldErrors => { + const fieldErrors: FieldErrors = {}; + for (const path in errors) { + const field = get(fields, path) as Field['_f'] | undefined; + set( + fieldErrors, + path, + Object.assign(errors[path], { ref: field && field.ref }), + ); + } + + return fieldErrors; +}; diff --git a/src/toNestObject.ts b/src/toNestObject.ts deleted file mode 100644 index 9b784ded..00000000 --- a/src/toNestObject.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { set, FieldError, FieldErrors } from 'react-hook-form'; - -export const toNestObject = ( - errors: Record, -): FieldErrors => { - const fieldErrors: FieldErrors = {}; - for (const path in errors) { - set(fieldErrors, path, errors[path]); - } - - return fieldErrors; -}; diff --git a/superstruct/src/__tests__/__fixtures__/data.ts b/superstruct/src/__tests__/__fixtures__/data.ts new file mode 100644 index 00000000..c69d51fc --- /dev/null +++ b/superstruct/src/__tests__/__fixtures__/data.ts @@ -0,0 +1,73 @@ +import { Field, InternalFieldName } from 'react-hook-form'; +import { + object, + number, + string, + optional, + pattern, + size, + union, + min, + max, + Infer, + define, + array, + boolean, +} from 'superstruct'; + +const Password = define('Password', (value, ctx) => + value === ctx.branch[0].password); + +export const schema = object({ + username: size(pattern(string(), /^\w+$/), 3, 30), + password: pattern(string(), /^[a-zA-Z0-9]{3,30}/), + repeatPassword: Password, + accessToken: optional(union([string(), number()])), + birthYear: optional(max(min(number(), 1900), 2013)), + email: optional(pattern(string(), /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/)), + tags: array(string()), + enabled: boolean(), + like: optional(array(object({ id: number(), name: size(string(), 4) }))), +}); + +export const validData: Infer = { + username: 'Doe', + password: 'Password123', + repeatPassword: 'Password123', + birthYear: 2000, + email: 'john@doe.com', + tags: ['tag1', 'tag2'], + enabled: true, + like: [ + { + id: 1, + name: 'name', + }, + ], +}; + +export const invalidData = { + password: '___', + email: '', + birthYear: 'birthYear', + like: [{ id: 'z' }], +}; + +export const fields: Record = { + username: { + ref: { name: 'username' }, + name: 'username', + }, + password: { + ref: { name: 'password' }, + name: 'password', + }, + email: { + ref: { name: 'email' }, + name: 'email', + }, + birthday: { + ref: { name: 'birthday' }, + name: 'birthday', + }, +}; diff --git a/superstruct/src/__tests__/__snapshots__/superstruct.ts.snap b/superstruct/src/__tests__/__snapshots__/superstruct.ts.snap index 6562231f..f556a4f3 100644 --- a/superstruct/src/__tests__/__snapshots__/superstruct.ts.snap +++ b/superstruct/src/__tests__/__snapshots__/superstruct.ts.snap @@ -5,30 +5,57 @@ Object { "errors": Object { "birthYear": Object { "message": "Expected a number, but received: \\"birthYear\\"", + "ref": undefined, "type": "number", }, "email": Object { "message": "Expected a string matching \`/^[\\\\w-\\\\.]+@([\\\\w-]+\\\\.)+[\\\\w-]{2,4}$/\` but received \\"\\"", + "ref": Object { + "name": "email", + }, "type": "string", }, "enabled": Object { "message": "Expected a value of type \`boolean\`, but received: \`undefined\`", + "ref": undefined, "type": "boolean", }, + "like": Array [ + Object { + "id": Object { + "message": "Expected a number, but received: \\"z\\"", + "ref": undefined, + "type": "number", + }, + "name": Object { + "message": "Expected a string, but received: undefined", + "ref": undefined, + "type": "string", + }, + }, + ], "password": Object { "message": "Expected a string matching \`/^[a-zA-Z0-9]{3,30}/\` but received \\"___\\"", + "ref": Object { + "name": "password", + }, "type": "string", }, "repeatPassword": Object { "message": "Expected a value of type \`Password\`, but received: \`undefined\`", + "ref": undefined, "type": "Password", }, "tags": Object { "message": "Expected an array value, but received: undefined", + "ref": undefined, "type": "array", }, "username": Object { "message": "Expected a string, but received: undefined", + "ref": Object { + "name": "username", + }, "type": "string", }, }, @@ -41,6 +68,7 @@ Object { "errors": Object { "birthYear": Object { "message": "Expected a number, but received: \\"birthYear\\"", + "ref": undefined, "type": "number", "types": Object { "number": "Expected a number, but received: \\"birthYear\\"", @@ -48,6 +76,9 @@ Object { }, "email": Object { "message": "Expected a string matching \`/^[\\\\w-\\\\.]+@([\\\\w-]+\\\\.)+[\\\\w-]{2,4}$/\` but received \\"\\"", + "ref": Object { + "name": "email", + }, "type": "string", "types": Object { "string": "Expected a string matching \`/^[\\\\w-\\\\.]+@([\\\\w-]+\\\\.)+[\\\\w-]{2,4}$/\` but received \\"\\"", @@ -55,13 +86,37 @@ Object { }, "enabled": Object { "message": "Expected a value of type \`boolean\`, but received: \`undefined\`", + "ref": undefined, "type": "boolean", "types": Object { "boolean": "Expected a value of type \`boolean\`, but received: \`undefined\`", }, }, + "like": Array [ + Object { + "id": Object { + "message": "Expected a number, but received: \\"z\\"", + "ref": undefined, + "type": "number", + "types": Object { + "number": "Expected a number, but received: \\"z\\"", + }, + }, + "name": Object { + "message": "Expected a string, but received: undefined", + "ref": undefined, + "type": "string", + "types": Object { + "string": "Expected a string, but received: undefined", + }, + }, + }, + ], "password": Object { "message": "Expected a string matching \`/^[a-zA-Z0-9]{3,30}/\` but received \\"___\\"", + "ref": Object { + "name": "password", + }, "type": "string", "types": Object { "string": "Expected a string matching \`/^[a-zA-Z0-9]{3,30}/\` but received \\"___\\"", @@ -69,6 +124,7 @@ Object { }, "repeatPassword": Object { "message": "Expected a value of type \`Password\`, but received: \`undefined\`", + "ref": undefined, "type": "Password", "types": Object { "Password": "Expected a value of type \`Password\`, but received: \`undefined\`", @@ -76,6 +132,7 @@ Object { }, "tags": Object { "message": "Expected an array value, but received: undefined", + "ref": undefined, "type": "array", "types": Object { "array": "Expected an array value, but received: undefined", @@ -83,6 +140,9 @@ Object { }, "username": Object { "message": "Expected a string, but received: undefined", + "ref": Object { + "name": "username", + }, "type": "string", "types": Object { "string": "Expected a string, but received: undefined", diff --git a/superstruct/src/__tests__/superstruct.ts b/superstruct/src/__tests__/superstruct.ts index 772a3d33..451275e2 100644 --- a/superstruct/src/__tests__/superstruct.ts +++ b/superstruct/src/__tests__/superstruct.ts @@ -1,76 +1,26 @@ -import { - object, - number, - string, - optional, - pattern, - size, - union, - min, - max, - Infer, - define, - array, - boolean, -} from 'superstruct'; import { superstructResolver } from '..'; - -const Password = define('Password', (value, ctx) => - value === ctx.branch[0].password); - -const schema = object({ - username: size(pattern(string(), /^\w+$/), 3, 30), - password: pattern(string(), /^[a-zA-Z0-9]{3,30}/), - repeatPassword: Password, - accessToken: optional(union([string(), number()])), - birthYear: optional(max(min(number(), 1900), 2013)), - email: optional(pattern(string(), /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/)), - tags: array(string()), - enabled: boolean(), -}); +import { invalidData, schema, validData, fields } from './__fixtures__/data'; describe('superstructResolver', () => { it('should return values from superstructResolver when validation pass', async () => { - const data: Infer = { - username: 'Doe', - password: 'Password123', - repeatPassword: 'Password123', - birthYear: 2000, - email: 'john@doe.com', - tags: ['tag1', 'tag2'], - enabled: true, - }; - - const result = await superstructResolver(schema)(data, undefined, { - fields: {}, + const result = await superstructResolver(schema)(validData, undefined, { + fields, }); - expect(result).toEqual({ errors: {}, values: data }); + expect(result).toEqual({ errors: {}, values: validData }); }); it('should return a single error from superstructResolver when validation fails', async () => { - const data = { - password: '___', - email: '', - birthYear: 'birthYear', - }; - - const result = await superstructResolver(schema)(data, undefined, { - fields: {}, + const result = await superstructResolver(schema)(invalidData, undefined, { + fields, }); expect(result).toMatchSnapshot(); }); it('should return all the errors from superstructResolver when validation fails with `validateAllFieldCriteria` set to true', async () => { - const data = { - password: '___', - email: '', - birthYear: 'birthYear', - }; - - const result = await superstructResolver(schema)(data, undefined, { - fields: {}, + const result = await superstructResolver(schema)(invalidData, undefined, { + fields, criteriaMode: 'all', }); diff --git a/superstruct/src/superstruct.ts b/superstruct/src/superstruct.ts index f9d7cb51..8e496198 100644 --- a/superstruct/src/superstruct.ts +++ b/superstruct/src/superstruct.ts @@ -1,5 +1,5 @@ import { appendErrors } from 'react-hook-form'; -import { toNestObject } from '@hookform/resolvers'; +import { toNestError } from '@hookform/resolvers'; import { StructError, validate } from 'superstruct'; import { convertArrayToPathName } from '@hookform/resolvers'; @@ -44,14 +44,17 @@ const parseErrorSchema = ( export const superstructResolver: Resolver = (schema, options) => async ( values, _context, - { criteriaMode }, + { criteriaMode, fields }, ) => { const [errors, result] = validate(values, schema, options); if (errors != null) { return { values: {}, - errors: toNestObject(parseErrorSchema(errors, criteriaMode === 'all')), + errors: toNestError( + parseErrorSchema(errors, criteriaMode === 'all'), + fields, + ), }; } diff --git a/vest/src/__tests__/__fixtures__/data.ts b/vest/src/__tests__/__fixtures__/data.ts new file mode 100644 index 00000000..c0e3f686 --- /dev/null +++ b/vest/src/__tests__/__fixtures__/data.ts @@ -0,0 +1,67 @@ +import { Field, InternalFieldName } from 'react-hook-form'; +import * as vest from 'vest'; + +export const validationSuite = vest.create('form', (data: any = {}) => { + vest.test('username', 'Username is required', () => { + vest.enforce(data.username).isNotEmpty(); + }); + + vest.test('username', 'Must be longer than 3 chars', () => { + vest.enforce(data.username).longerThan(3); + }); + + vest.test('deepObject.data', 'deepObject.data is required', () => { + vest.enforce(data.deepObject.data).isNotEmpty(); + }); + + vest.test('password', 'Password is required', () => { + vest.enforce(data.password).isNotEmpty(); + }); + + vest.test('password', 'Password must be at least 5 chars', () => { + vest.enforce(data.password).longerThanOrEquals(5); + }); + + vest.test('password', 'Password must contain a digit', () => { + vest.enforce(data.password).matches(/[0-9]/); + }); + + vest.test('password', 'Password must contain a symbol', () => { + vest.enforce(data.password).matches(/[^A-Za-z0-9]/); + }); +}); + +export const validData = { + username: 'asdda', + password: 'asddfg123!', + deepObject: { + data: 'test', + }, +}; + +export const invalidData = { + username: '', + password: 'a', + deepObject: { + data: '', + }, +}; + +export const fields: Record = { + username: { + ref: { name: 'username' }, + name: 'username', + }, + password: { + ref: { name: 'password' }, + name: 'password', + }, + email: { + ref: { name: 'email' }, + name: 'email', + }, + birthday: { + ref: { name: 'birthday' }, + name: 'birthday', + }, +}; diff --git a/vest/src/__tests__/__snapshots__/vest.ts.snap b/vest/src/__tests__/__snapshots__/vest.ts.snap index 9334be13..4f5dd963 100644 --- a/vest/src/__tests__/__snapshots__/vest.ts.snap +++ b/vest/src/__tests__/__snapshots__/vest.ts.snap @@ -6,6 +6,7 @@ Object { "deepObject": Object { "data": Object { "message": "deepObject.data is required", + "ref": undefined, "type": "", "types": Object { "0": "deepObject.data is required", @@ -14,6 +15,9 @@ Object { }, "password": Object { "message": "Password must be at least 5 chars", + "ref": Object { + "name": "password", + }, "type": "", "types": Object { "0": "Password must be at least 5 chars", @@ -23,6 +27,9 @@ Object { }, "username": Object { "message": "Username is required", + "ref": Object { + "name": "username", + }, "type": "", "types": Object { "0": "Username is required", @@ -40,6 +47,7 @@ Object { "deepObject": Object { "data": Object { "message": "deepObject.data is required", + "ref": undefined, "type": "", "types": Object { "0": "deepObject.data is required", @@ -48,6 +56,9 @@ Object { }, "password": Object { "message": "Password must be at least 5 chars", + "ref": Object { + "name": "password", + }, "type": "", "types": Object { "0": "Password must be at least 5 chars", @@ -57,6 +68,9 @@ Object { }, "username": Object { "message": "Username is required", + "ref": Object { + "name": "username", + }, "type": "", "types": Object { "0": "Username is required", @@ -74,15 +88,22 @@ Object { "deepObject": Object { "data": Object { "message": "deepObject.data is required", + "ref": undefined, "type": "", }, }, "password": Object { "message": "Password must be at least 5 chars", + "ref": Object { + "name": "password", + }, "type": "", }, "username": Object { "message": "Username is required", + "ref": Object { + "name": "username", + }, "type": "", }, }, @@ -96,15 +117,22 @@ Object { "deepObject": Object { "data": Object { "message": "deepObject.data is required", + "ref": undefined, "type": "", }, }, "password": Object { "message": "Password must be at least 5 chars", + "ref": Object { + "name": "password", + }, "type": "", }, "username": Object { "message": "Username is required", + "ref": Object { + "name": "username", + }, "type": "", }, }, diff --git a/vest/src/__tests__/vest.ts b/vest/src/__tests__/vest.ts index 604e6512..16bb307e 100644 --- a/vest/src/__tests__/vest.ts +++ b/vest/src/__tests__/vest.ts @@ -1,137 +1,64 @@ -import * as vest from 'vest'; import { vestResolver } from '..'; - -const validationSuite = vest.create('form', (data: any = {}) => { - vest.test('username', 'Username is required', () => { - vest.enforce(data.username).isNotEmpty(); - }); - - vest.test('username', 'Must be longer than 3 chars', () => { - vest.enforce(data.username).longerThan(3); - }); - - vest.test('deepObject.data', 'deepObject.data is required', () => { - vest.enforce(data.deepObject.data).isNotEmpty(); - }); - - vest.test('password', 'Password is required', () => { - vest.enforce(data.password).isNotEmpty(); - }); - - vest.test('password', 'Password must be at least 5 chars', () => { - vest.enforce(data.password).longerThanOrEquals(5); - }); - - vest.test('password', 'Password must contain a digit', () => { - vest.enforce(data.password).matches(/[0-9]/); - }); - - vest.test('password', 'Password must contain a symbol', () => { - vest.enforce(data.password).matches(/[^A-Za-z0-9]/); - }); -}); +import { + invalidData, + validationSuite, + validData, + fields, +} from './__fixtures__/data'; describe('vestResolver', () => { it('should return values from vestResolver when validation pass', async () => { - const data = { - username: 'asdda', - password: 'asddfg123!', - deepObject: { - data: 'test', - }, - }; expect( - await vestResolver(validationSuite)(data, undefined, { fields: {} }), + await vestResolver(validationSuite)(validData, undefined, { fields }), ).toEqual({ - values: data, + values: validData, errors: {}, }); }); it('should return values from vestResolver with `mode: sync` when validation pass', async () => { - const data = { - username: 'asdda', - password: 'asddfg123!', - deepObject: { - data: 'test', - }, - }; expect( - await vestResolver(validationSuite, undefined, { mode: 'sync' })( - data, - undefined, - { fields: {} }, - ), + await vestResolver(validationSuite, undefined, { + mode: 'sync', + })(validData, undefined, { fields }), ).toEqual({ - values: data, + values: validData, errors: {}, }); }); it('should return single error message from vestResolver when validation fails and validateAllFieldCriteria set to false', async () => { - const data = { - username: '', - password: 'a', - deepObject: { - data: '', - }, - }; - expect( - await vestResolver(validationSuite)(data, undefined, { fields: {} }), + await vestResolver(validationSuite)(invalidData, undefined, { + fields, + }), ).toMatchSnapshot(); }); it('should return single error message from vestResolver when validation fails and validateAllFieldCriteria set to false and `mode: sync`', async () => { - const data = { - username: '', - password: 'a', - deepObject: { - data: '', - }, - }; - expect( - await vestResolver(validationSuite, undefined, { mode: 'sync' })( - data, - undefined, - { fields: {} }, - ), + await vestResolver(validationSuite, undefined, { + mode: 'sync', + })(invalidData, undefined, { fields }), ).toMatchSnapshot(); }); it('should return all the error messages from vestResolver when validation fails and validateAllFieldCriteria set to true', async () => { - const data = { - username: '', - password: 'a', - deepObject: { - data: '', - }, - }; - expect( await vestResolver(validationSuite)( - data, + invalidData, {}, - { fields: {}, criteriaMode: 'all' }, + { fields, criteriaMode: 'all' }, ), ).toMatchSnapshot(); }); it('should return all the error messages from vestResolver when validation fails and validateAllFieldCriteria set to true and `mode: sync`', async () => { - const data = { - username: '', - password: 'a', - deepObject: { - data: '', - }, - }; - expect( await vestResolver(validationSuite, undefined, { mode: 'sync' })( - data, + invalidData, {}, - { fields: {}, criteriaMode: 'all' }, + { fields, criteriaMode: 'all' }, ), ).toMatchSnapshot(); }); diff --git a/vest/src/vest.ts b/vest/src/vest.ts index 5d85e085..44c510e6 100644 --- a/vest/src/vest.ts +++ b/vest/src/vest.ts @@ -1,4 +1,4 @@ -import { toNestObject } from '@hookform/resolvers'; +import { toNestError } from '@hookform/resolvers'; import promisify from 'vest/promisify'; import { DraftResult, IVestResult } from 'vest/vestResult'; import type { VestErrors, Resolver } from './types'; @@ -32,7 +32,7 @@ export const vestResolver: Resolver = ( schema, _, { mode } = { mode: 'async' }, -) => async (values, _context, { criteriaMode }) => { +) => async (values, _context, { criteriaMode, fields }) => { let result: IVestResult | DraftResult; if (mode === 'async') { const validateSchema = promisify(schema); @@ -49,6 +49,9 @@ export const vestResolver: Resolver = ( return { values: {}, - errors: toNestObject(parseErrorSchema(errors, criteriaMode === 'all')), + errors: toNestError( + parseErrorSchema(errors, criteriaMode === 'all'), + fields, + ), }; }; diff --git a/yup/src/__tests__/__fixtures__/data.ts b/yup/src/__tests__/__fixtures__/data.ts new file mode 100644 index 00000000..9499b94a --- /dev/null +++ b/yup/src/__tests__/__fixtures__/data.ts @@ -0,0 +1,65 @@ +import { Field, InternalFieldName } from 'react-hook-form'; +import * as yup from 'yup'; + +export const schema = yup.object({ + username: yup.string().matches(/^\w+$/).min(3).max(30).required(), + password: yup + .string() + .matches(/^[a-zA-Z0-9]{3,30}/) + .required(), + repeatPassword: yup.ref('password'), + accessToken: yup.string(), + birthYear: yup.number().min(1900).max(2013), + email: yup.string().email(), + tags: yup.array(yup.string()), + enabled: yup.boolean(), + like: yup.array().of( + yup.object({ + id: yup.number().required(), + name: yup.string().length(4).required(), + }), + ), +}); + +export const validData: yup.InferType = { + username: 'Doe', + password: 'Password123', + repeatPassword: 'Password123', + birthYear: 2000, + email: 'john@doe.com', + tags: ['tag1', 'tag2'], + enabled: true, + accessToken: 'accessToken', + like: [ + { + id: 1, + name: 'name', + }, + ], +}; + +export const invalidData = { + password: '___', + email: '', + birthYear: 'birthYear', + like: [{ id: 'z' }], +}; + +export const fields: Record = { + username: { + ref: { name: 'username' }, + name: 'username', + }, + password: { + ref: { name: 'password' }, + name: 'password', + }, + email: { + ref: { name: 'email' }, + name: 'email', + }, + birthday: { + ref: { name: 'birthday' }, + name: 'birthday', + }, +}; diff --git a/yup/src/__tests__/__snapshots__/yup.ts.snap b/yup/src/__tests__/__snapshots__/yup.ts.snap index 9fd474b2..5e8da723 100644 --- a/yup/src/__tests__/__snapshots__/yup.ts.snap +++ b/yup/src/__tests__/__snapshots__/yup.ts.snap @@ -5,14 +5,35 @@ Object { "errors": Object { "birthYear": Object { "message": "birthYear must be a \`number\` type, but the final value was: \`NaN\` (cast from the value \`\\"birthYear\\"\`).", + "ref": undefined, "type": "typeError", }, + "like": Array [ + Object { + "id": Object { + "message": "like[0].id must be a \`number\` type, but the final value was: \`NaN\` (cast from the value \`\\"z\\"\`).", + "ref": undefined, + "type": "typeError", + }, + "name": Object { + "message": "like[0].name is a required field", + "ref": undefined, + "type": "required", + }, + }, + ], "password": Object { "message": "password must match the following: \\"/^[a-zA-Z0-9]{3,30}/\\"", + "ref": Object { + "name": "password", + }, "type": "matches", }, "username": Object { "message": "username is a required field", + "ref": Object { + "name": "username", + }, "type": "required", }, }, @@ -25,14 +46,35 @@ Object { "errors": Object { "birthYear": Object { "message": "birthYear must be a \`number\` type, but the final value was: \`NaN\` (cast from the value \`\\"birthYear\\"\`).", + "ref": undefined, "type": "typeError", }, + "like": Array [ + Object { + "id": Object { + "message": "like[0].id must be a \`number\` type, but the final value was: \`NaN\` (cast from the value \`\\"z\\"\`).", + "ref": undefined, + "type": "typeError", + }, + "name": Object { + "message": "like[0].name is a required field", + "ref": undefined, + "type": "required", + }, + }, + ], "password": Object { "message": "password must match the following: \\"/^[a-zA-Z0-9]{3,30}/\\"", + "ref": Object { + "name": "password", + }, "type": "matches", }, "username": Object { "message": "username is a required field", + "ref": Object { + "name": "username", + }, "type": "required", }, }, @@ -45,13 +87,37 @@ Object { "errors": Object { "birthYear": Object { "message": "birthYear must be a \`number\` type, but the final value was: \`NaN\` (cast from the value \`\\"birthYear\\"\`).", + "ref": undefined, "type": "typeError", "types": Object { "typeError": "birthYear must be a \`number\` type, but the final value was: \`NaN\` (cast from the value \`\\"birthYear\\"\`).", }, }, + "like": Array [ + Object { + "id": Object { + "message": "like[0].id must be a \`number\` type, but the final value was: \`NaN\` (cast from the value \`\\"z\\"\`).", + "ref": undefined, + "type": "typeError", + "types": Object { + "typeError": "like[0].id must be a \`number\` type, but the final value was: \`NaN\` (cast from the value \`\\"z\\"\`).", + }, + }, + "name": Object { + "message": "like[0].name is a required field", + "ref": undefined, + "type": "required", + "types": Object { + "required": "like[0].name is a required field", + }, + }, + }, + ], "password": Object { "message": "password must match the following: \\"/^[a-zA-Z0-9]{3,30}/\\"", + "ref": Object { + "name": "password", + }, "type": "matches", "types": Object { "matches": "password must match the following: \\"/^[a-zA-Z0-9]{3,30}/\\"", @@ -59,6 +125,9 @@ Object { }, "username": Object { "message": "username is a required field", + "ref": Object { + "name": "username", + }, "type": "required", "types": Object { "required": "username is a required field", @@ -74,13 +143,37 @@ Object { "errors": Object { "birthYear": Object { "message": "birthYear must be a \`number\` type, but the final value was: \`NaN\` (cast from the value \`\\"birthYear\\"\`).", + "ref": undefined, "type": "typeError", "types": Object { "typeError": "birthYear must be a \`number\` type, but the final value was: \`NaN\` (cast from the value \`\\"birthYear\\"\`).", }, }, + "like": Array [ + Object { + "id": Object { + "message": "like[0].id must be a \`number\` type, but the final value was: \`NaN\` (cast from the value \`\\"z\\"\`).", + "ref": undefined, + "type": "typeError", + "types": Object { + "typeError": "like[0].id must be a \`number\` type, but the final value was: \`NaN\` (cast from the value \`\\"z\\"\`).", + }, + }, + "name": Object { + "message": "like[0].name is a required field", + "ref": undefined, + "type": "required", + "types": Object { + "required": "like[0].name is a required field", + }, + }, + }, + ], "password": Object { "message": "password must match the following: \\"/^[a-zA-Z0-9]{3,30}/\\"", + "ref": Object { + "name": "password", + }, "type": "matches", "types": Object { "matches": "password must match the following: \\"/^[a-zA-Z0-9]{3,30}/\\"", @@ -88,6 +181,9 @@ Object { }, "username": Object { "message": "username is a required field", + "ref": Object { + "name": "username", + }, "type": "required", "types": Object { "required": "username is a required field", @@ -103,6 +199,7 @@ Object { "errors": Object { "name": Object { "message": "name must be at least 6 characters", + "ref": undefined, "type": "min", }, }, @@ -115,6 +212,7 @@ Object { "errors": Object { "required": Object { "message": "error1", + "ref": undefined, "type": "required", }, }, @@ -127,6 +225,7 @@ Object { "errors": Object { "name": Object { "message": "Email or name are required", + "ref": undefined, "type": "name", }, }, diff --git a/yup/src/__tests__/yup.ts b/yup/src/__tests__/yup.ts index dec6d998..2ec95eb1 100644 --- a/yup/src/__tests__/yup.ts +++ b/yup/src/__tests__/yup.ts @@ -1,97 +1,50 @@ /* eslint-disable no-console, @typescript-eslint/ban-ts-comment */ import * as yup from 'yup'; import { yupResolver } from '..'; - -const schema = yup.object({ - username: yup.string().matches(/^\w+$/).min(3).max(30).required(), - password: yup - .string() - .matches(/^[a-zA-Z0-9]{3,30}/) - .required(), - repeatPassword: yup.ref('password'), - accessToken: yup.string(), - birthYear: yup.number().min(1900).max(2013), - email: yup.string().email(), - tags: yup.array(yup.string()), - enabled: yup.boolean(), -}); +import { schema, validData, fields, invalidData } from './__fixtures__/data'; describe('yupResolver', () => { it('should return values from yupResolver when validation pass', async () => { - const data: yup.InferType = { - username: 'Doe', - password: 'Password123', - repeatPassword: 'Password123', - birthYear: 2000, - email: 'john@doe.com', - tags: ['tag1', 'tag2'], - enabled: true, - accessToken: 'accessToken', - }; - const schemaSpy = jest.spyOn(schema, 'validate'); const schemaSyncSpy = jest.spyOn(schema, 'validateSync'); - const result = await yupResolver(schema)(data, undefined, { fields: {} }); + const result = await yupResolver(schema)(validData, undefined, { + fields, + }); expect(schemaSpy).toHaveBeenCalledTimes(1); expect(schemaSyncSpy).not.toHaveBeenCalled(); - expect(result).toEqual({ errors: {}, values: data }); + expect(result).toEqual({ errors: {}, values: validData }); }); it('should return values from yupResolver with `mode: sync` when validation pass', async () => { - const data: yup.InferType = { - username: 'Doe', - password: 'Password123', - repeatPassword: 'Password123', - birthYear: 2000, - email: 'john@doe.com', - tags: ['tag1', 'tag2'], - enabled: true, - accessToken: 'accessToken', - }; - const validateSyncSpy = jest.spyOn(schema, 'validateSync'); const validateSpy = jest.spyOn(schema, 'validate'); - const result = await yupResolver(schema, undefined, { mode: 'sync' })( - data, - undefined, - { fields: {} }, - ); + const result = await yupResolver(schema, undefined, { + mode: 'sync', + })(validData, undefined, { fields }); expect(validateSyncSpy).toHaveBeenCalledTimes(1); expect(validateSpy).not.toHaveBeenCalled(); - expect(result).toEqual({ errors: {}, values: data }); + expect(result).toEqual({ errors: {}, values: validData }); }); it('should return a single error from yupResolver when validation fails', async () => { - const data = { - password: '___', - email: '', - birthYear: 'birthYear', - }; - - const result = await yupResolver(schema)(data, undefined, { fields: {} }); + const result = await yupResolver(schema)(invalidData, undefined, { + fields, + }); expect(result).toMatchSnapshot(); }); it('should return a single error from yupResolver with `mode: sync` when validation fails', async () => { - const data = { - password: '___', - email: '', - birthYear: 'birthYear', - }; - const validateSyncSpy = jest.spyOn(schema, 'validateSync'); const validateSpy = jest.spyOn(schema, 'validate'); - const result = await yupResolver(schema, undefined, { mode: 'sync' })( - data, - undefined, - { fields: {} }, - ); + const result = await yupResolver(schema, undefined, { + mode: 'sync', + })(invalidData, undefined, { fields }); expect(validateSyncSpy).toHaveBeenCalledTimes(1); expect(validateSpy).not.toHaveBeenCalled(); @@ -99,14 +52,8 @@ describe('yupResolver', () => { }); it('should return all the errors from yupResolver when validation fails with `validateAllFieldCriteria` set to true', async () => { - const data = { - password: '___', - email: '', - birthYear: 'birthYear', - }; - - const result = await yupResolver(schema)(data, undefined, { - fields: {}, + const result = await yupResolver(schema)(invalidData, undefined, { + fields, criteriaMode: 'all', }); @@ -114,17 +61,11 @@ describe('yupResolver', () => { }); it('should return all the errors from yupResolver when validation fails with `validateAllFieldCriteria` set to true and `mode: sync`', async () => { - const data = { - password: '___', - email: '', - birthYear: 'birthYear', - }; - const result = await yupResolver(schema, undefined, { mode: 'sync' })( - data, + invalidData, undefined, { - fields: {}, + fields, criteriaMode: 'all', }, ); @@ -147,7 +88,7 @@ describe('yupResolver', () => { const validateSpy = jest.spyOn(schemaWithContext, 'validate'); const result = await yupResolver(schemaWithContext)(data, context, { - fields: {}, + fields, }); expect(validateSpy).toHaveBeenCalledTimes(1); expect(validateSpy).toHaveBeenCalledWith( @@ -170,7 +111,7 @@ describe('yupResolver', () => { }); const result = await yupResolver(yupSchema)({ name: '' }, undefined, { - fields: {}, + fields, }); expect(result).toMatchSnapshot(); }); @@ -183,7 +124,7 @@ describe('yupResolver', () => { {}, undefined, { - fields: {}, + fields, }, ); expect(console.warn).toHaveBeenCalledWith( @@ -199,7 +140,7 @@ describe('yupResolver', () => { await yupResolver(yup.object(), { context: { noContext: true } })( {}, undefined, - { fields: {} }, + { fields }, ); expect(console.warn).not.toHaveBeenCalled(); process.env.NODE_ENV = 'test'; @@ -217,7 +158,7 @@ describe('yupResolver', () => { 'Email or name are required', (value) => !!(value && (value.name || value.email)), ), - )({ name: '', email: '' }, undefined, { fields: {} }); + )({ name: '', email: '' }, undefined, { fields }); expect(result).toMatchSnapshot(); }); diff --git a/yup/src/yup.ts b/yup/src/yup.ts index b43c26f6..b5ebc560 100644 --- a/yup/src/yup.ts +++ b/yup/src/yup.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import Yup from 'yup'; -import { toNestObject } from '@hookform/resolvers'; +import { toNestError } from '@hookform/resolvers'; import { Resolver } from './types'; /** @@ -60,7 +60,7 @@ export const yupResolver: Resolver = ( abortEarly: false, }, { mode } = { mode: 'async' }, -) => async (values, context, { criteriaMode }) => { +) => async (values, context, { criteriaMode, fields }) => { try { if (options.context && process.env.NODE_ENV === 'development') { // eslint-disable-next-line no-console @@ -89,7 +89,7 @@ export const yupResolver: Resolver = ( return { values: {}, - errors: toNestObject(parsedErrors), + errors: toNestError(parsedErrors, fields), }; } }; diff --git a/zod/src/__tests__/__fixtures__/data.ts b/zod/src/__tests__/__fixtures__/data.ts index 0c2f5f83..dde47bb8 100644 --- a/zod/src/__tests__/__fixtures__/data.ts +++ b/zod/src/__tests__/__fixtures__/data.ts @@ -1,3 +1,4 @@ +import { Field, InternalFieldName } from 'react-hook-form'; import * as z from 'zod'; export const schema = z @@ -46,3 +47,22 @@ export const invalidData = { birthYear: 'birthYear', like: [{ id: 'z' }], }; + +export const fields: Record = { + username: { + ref: { name: 'username' }, + name: 'username', + }, + password: { + ref: { name: 'password' }, + name: 'password', + }, + email: { + ref: { name: 'email' }, + name: 'email', + }, + birthday: { + ref: { name: 'birthday' }, + name: 'birthday', + }, +}; diff --git a/zod/src/__tests__/__snapshots__/zod.ts.snap b/zod/src/__tests__/__snapshots__/zod.ts.snap index b7134ac1..00a2f1b3 100644 --- a/zod/src/__tests__/__snapshots__/zod.ts.snap +++ b/zod/src/__tests__/__snapshots__/zod.ts.snap @@ -5,48 +5,65 @@ Object { "errors": Object { "birthYear": Object { "message": "Invalid input", + "ref": undefined, "type": "invalid_union", }, "confirm": Object { "message": "Passwords don't match", + "ref": undefined, "type": "custom_error", }, "email": Object { "message": "Invalid email", + "ref": Object { + "name": "email", + }, "type": "invalid_string", }, "enabled": Object { "message": "Required", + "ref": undefined, "type": "invalid_type", }, "like": Object { "0": Object { "id": Object { "message": "Expected number, received string", + "ref": undefined, "type": "invalid_type", }, "name": Object { "message": "Required", + "ref": undefined, "type": "invalid_type", }, }, "message": "Invalid input", + "ref": undefined, "type": "invalid_union", }, "password": Object { "message": "Invalid", + "ref": Object { + "name": "password", + }, "type": "invalid_string", }, "repeatPassword": Object { "message": "Required", + "ref": undefined, "type": "invalid_type", }, "tags": Object { "message": "Required", + "ref": undefined, "type": "invalid_type", }, "username": Object { "message": "Required", + "ref": Object { + "name": "username", + }, "type": "invalid_type", }, }, @@ -59,48 +76,65 @@ Object { "errors": Object { "birthYear": Object { "message": "Invalid input", + "ref": undefined, "type": "invalid_union", }, "confirm": Object { "message": "Passwords don't match", + "ref": undefined, "type": "custom_error", }, "email": Object { "message": "Invalid email", + "ref": Object { + "name": "email", + }, "type": "invalid_string", }, "enabled": Object { "message": "Required", + "ref": undefined, "type": "invalid_type", }, "like": Object { "0": Object { "id": Object { "message": "Expected number, received string", + "ref": undefined, "type": "invalid_type", }, "name": Object { "message": "Required", + "ref": undefined, "type": "invalid_type", }, }, "message": "Invalid input", + "ref": undefined, "type": "invalid_union", }, "password": Object { "message": "Invalid", + "ref": Object { + "name": "password", + }, "type": "invalid_string", }, "repeatPassword": Object { "message": "Required", + "ref": undefined, "type": "invalid_type", }, "tags": Object { "message": "Required", + "ref": undefined, "type": "invalid_type", }, "username": Object { "message": "Required", + "ref": Object { + "name": "username", + }, "type": "invalid_type", }, }, @@ -113,6 +147,7 @@ Object { "errors": Object { "birthYear": Object { "message": "Invalid input", + "ref": undefined, "type": "invalid_union", "types": Object { "invalid_type": "Expected undefined, received string", @@ -121,6 +156,7 @@ Object { }, "confirm": Object { "message": "Passwords don't match", + "ref": undefined, "type": "custom_error", "types": Object { "custom_error": "Passwords don't match", @@ -128,6 +164,9 @@ Object { }, "email": Object { "message": "Invalid email", + "ref": Object { + "name": "email", + }, "type": "invalid_string", "types": Object { "invalid_string": "Invalid email", @@ -135,6 +174,7 @@ Object { }, "enabled": Object { "message": "Required", + "ref": undefined, "type": "invalid_type", "types": Object { "invalid_type": "Required", @@ -144,6 +184,7 @@ Object { "0": Object { "id": Object { "message": "Expected number, received string", + "ref": undefined, "type": "invalid_type", "types": Object { "invalid_type": "Expected number, received string", @@ -151,6 +192,7 @@ Object { }, "name": Object { "message": "Required", + "ref": undefined, "type": "invalid_type", "types": Object { "invalid_type": "Required", @@ -158,6 +200,7 @@ Object { }, }, "message": "Invalid input", + "ref": undefined, "type": "invalid_union", "types": Object { "invalid_type": "Expected undefined, received array", @@ -166,6 +209,9 @@ Object { }, "password": Object { "message": "Invalid", + "ref": Object { + "name": "password", + }, "type": "invalid_string", "types": Object { "invalid_string": "Invalid", @@ -173,6 +219,7 @@ Object { }, "repeatPassword": Object { "message": "Required", + "ref": undefined, "type": "invalid_type", "types": Object { "invalid_type": "Required", @@ -180,6 +227,7 @@ Object { }, "tags": Object { "message": "Required", + "ref": undefined, "type": "invalid_type", "types": Object { "invalid_type": "Required", @@ -187,6 +235,9 @@ Object { }, "username": Object { "message": "Required", + "ref": Object { + "name": "username", + }, "type": "invalid_type", "types": Object { "invalid_type": "Required", @@ -202,6 +253,7 @@ Object { "errors": Object { "birthYear": Object { "message": "Invalid input", + "ref": undefined, "type": "invalid_union", "types": Object { "invalid_type": "Expected undefined, received string", @@ -210,6 +262,7 @@ Object { }, "confirm": Object { "message": "Passwords don't match", + "ref": undefined, "type": "custom_error", "types": Object { "custom_error": "Passwords don't match", @@ -217,6 +270,9 @@ Object { }, "email": Object { "message": "Invalid email", + "ref": Object { + "name": "email", + }, "type": "invalid_string", "types": Object { "invalid_string": "Invalid email", @@ -224,6 +280,7 @@ Object { }, "enabled": Object { "message": "Required", + "ref": undefined, "type": "invalid_type", "types": Object { "invalid_type": "Required", @@ -233,6 +290,7 @@ Object { "0": Object { "id": Object { "message": "Expected number, received string", + "ref": undefined, "type": "invalid_type", "types": Object { "invalid_type": "Expected number, received string", @@ -240,6 +298,7 @@ Object { }, "name": Object { "message": "Required", + "ref": undefined, "type": "invalid_type", "types": Object { "invalid_type": "Required", @@ -247,6 +306,7 @@ Object { }, }, "message": "Invalid input", + "ref": undefined, "type": "invalid_union", "types": Object { "invalid_type": "Expected undefined, received array", @@ -255,6 +315,9 @@ Object { }, "password": Object { "message": "Invalid", + "ref": Object { + "name": "password", + }, "type": "invalid_string", "types": Object { "invalid_string": "Invalid", @@ -262,6 +325,7 @@ Object { }, "repeatPassword": Object { "message": "Required", + "ref": undefined, "type": "invalid_type", "types": Object { "invalid_type": "Required", @@ -269,6 +333,7 @@ Object { }, "tags": Object { "message": "Required", + "ref": undefined, "type": "invalid_type", "types": Object { "invalid_type": "Required", @@ -276,6 +341,9 @@ Object { }, "username": Object { "message": "Required", + "ref": Object { + "name": "username", + }, "type": "invalid_type", "types": Object { "invalid_type": "Required", diff --git a/zod/src/__tests__/zod.ts b/zod/src/__tests__/zod.ts index bf2cda92..05b53df6 100644 --- a/zod/src/__tests__/zod.ts +++ b/zod/src/__tests__/zod.ts @@ -1,12 +1,12 @@ import { zodResolver } from '..'; -import { schema, validData, invalidData } from './__fixtures__/data'; +import { schema, validData, invalidData, fields } from './__fixtures__/data'; describe('zodResolver', () => { it('should return values from zodResolver when validation pass', async () => { const parseAsyncSpy = jest.spyOn(schema, 'parseAsync'); const result = await zodResolver(schema)(validData, undefined, { - fields: {}, + fields, }); expect(parseAsyncSpy).toHaveBeenCalledTimes(1); @@ -19,7 +19,7 @@ describe('zodResolver', () => { const result = await zodResolver(schema, undefined, { mode: 'sync', - })(validData, undefined, { fields: {} }); + })(validData, undefined, { fields }); expect(parseSpy).toHaveBeenCalledTimes(1); expect(parseAsyncSpy).not.toHaveBeenCalled(); @@ -28,7 +28,7 @@ describe('zodResolver', () => { it('should return a single error from zodResolver when validation fails', async () => { const result = await zodResolver(schema)(invalidData, undefined, { - fields: {}, + fields, }); expect(result).toMatchSnapshot(); @@ -40,7 +40,7 @@ describe('zodResolver', () => { const result = await zodResolver(schema, undefined, { mode: 'sync', - })(invalidData, undefined, { fields: {} }); + })(invalidData, undefined, { fields }); expect(parseSpy).toHaveBeenCalledTimes(1); expect(parseAsyncSpy).not.toHaveBeenCalled(); @@ -49,7 +49,7 @@ describe('zodResolver', () => { it('should return all the errors from zodResolver when validation fails with `validateAllFieldCriteria` set to true', async () => { const result = await zodResolver(schema)(invalidData, undefined, { - fields: {}, + fields, criteriaMode: 'all', }); @@ -61,7 +61,7 @@ describe('zodResolver', () => { invalidData, undefined, { - fields: {}, + fields, criteriaMode: 'all', }, ); diff --git a/zod/src/zod.ts b/zod/src/zod.ts index 1f476973..3b4adf58 100644 --- a/zod/src/zod.ts +++ b/zod/src/zod.ts @@ -1,6 +1,6 @@ import { appendErrors, FieldError } from 'react-hook-form'; import * as z from 'zod'; -import { toNestObject } from '@hookform/resolvers'; +import { toNestError } from '@hookform/resolvers'; import type { Resolver } from './types'; const parseErrorSchema = ( @@ -56,8 +56,9 @@ export const zodResolver: Resolver = ( values: {}, errors: error.isEmpty ? {} - : toNestObject( + : toNestError( parseErrorSchema(error.errors, options.criteriaMode === 'all'), + options.fields, ), }; }