From 6b4259f6dd79897f763c220d0e42e38fb195f2fa Mon Sep 17 00:00:00 2001 From: jorisre Date: Tue, 2 Feb 2021 23:07:40 +0100 Subject: [PATCH 1/9] test: extract test's fixtures --- .../src/__tests__/__fixtures__/data.ts | 53 +++++++++++++++ superstruct/src/__tests__/superstruct.ts | 67 ++----------------- 2 files changed, 57 insertions(+), 63 deletions(-) create mode 100644 superstruct/src/__tests__/__fixtures__/data.ts diff --git a/superstruct/src/__tests__/__fixtures__/data.ts b/superstruct/src/__tests__/__fixtures__/data.ts new file mode 100644 index 00000000..de3a8ca7 --- /dev/null +++ b/superstruct/src/__tests__/__fixtures__/data.ts @@ -0,0 +1,53 @@ +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' }], +}; diff --git a/superstruct/src/__tests__/superstruct.ts b/superstruct/src/__tests__/superstruct.ts index 772a3d33..874e067f 100644 --- a/superstruct/src/__tests__/superstruct.ts +++ b/superstruct/src/__tests__/superstruct.ts @@ -1,77 +1,18 @@ -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 } 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, { + 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: {}, - }); - - 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, { + const result = await superstructResolver(schema)(invalidData, undefined, { fields: {}, - criteriaMode: 'all', }); expect(result).toMatchSnapshot(); From d081ff4c58a559d646cb60c68eff61b050410050 Mon Sep 17 00:00:00 2001 From: jorisre Date: Tue, 2 Feb 2021 23:07:59 +0100 Subject: [PATCH 2/9] test: jest fixtures config --- jest.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/jest.config.js b/jest.config.js index 0445b64d..d74421af 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,6 +3,7 @@ module.exports = { testEnvironment: 'jsdom', restoreMocks: true, testMatch: ['**/__tests__/**/*.+(js|jsx|ts|tsx)'], + testPathIgnorePatterns: ['/__fixtures__/'], transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$'], moduleNameMapper: { '^@hookform/resolvers$': '/src', From 73d9cb9f5f07b269be42435682337f8ca9105d2d Mon Sep 17 00:00:00 2001 From: jorisre Date: Tue, 2 Feb 2021 23:09:07 +0100 Subject: [PATCH 3/9] perf: reduce superstruct resolver size --- superstruct/package.json | 3 +- .../__snapshots__/superstruct.ts.snap | 67 +++---------------- superstruct/src/superstruct.ts | 66 ++++-------------- superstruct/src/types.ts | 2 +- 4 files changed, 28 insertions(+), 110 deletions(-) diff --git a/superstruct/package.json b/superstruct/package.json index 8714cae9..f42ea659 100644 --- a/superstruct/package.json +++ b/superstruct/package.json @@ -12,6 +12,7 @@ "license": "MIT", "peerDependencies": { "react-hook-form": ">=6.6.0", - "@hookform/resolvers": "^1.0.0" + "@hookform/resolvers": "^1.0.0", + "superstruct": ">+0.12.0" } } diff --git a/superstruct/src/__tests__/__snapshots__/superstruct.ts.snap b/superstruct/src/__tests__/__snapshots__/superstruct.ts.snap index 6562231f..8825d9fd 100644 --- a/superstruct/src/__tests__/__snapshots__/superstruct.ts.snap +++ b/superstruct/src/__tests__/__snapshots__/superstruct.ts.snap @@ -15,78 +15,33 @@ Object { "message": "Expected a value of type \`boolean\`, but received: \`undefined\`", "type": "boolean", }, - "password": Object { - "message": "Expected a string matching \`/^[a-zA-Z0-9]{3,30}/\` but received \\"___\\"", - "type": "string", - }, - "repeatPassword": Object { - "message": "Expected a value of type \`Password\`, but received: \`undefined\`", - "type": "Password", - }, - "tags": Object { - "message": "Expected an array value, but received: undefined", - "type": "array", - }, - "username": Object { - "message": "Expected a string, but received: undefined", - "type": "string", - }, - }, - "values": Object {}, -} -`; - -exports[`superstructResolver should return all the errors from superstructResolver when validation fails with \`validateAllFieldCriteria\` set to true 1`] = ` -Object { - "errors": Object { - "birthYear": Object { - "message": "Expected a number, but received: \\"birthYear\\"", - "type": "number", - "types": Object { - "number": "Expected a number, but received: \\"birthYear\\"", - }, - }, - "email": Object { - "message": "Expected a string matching \`/^[\\\\w-\\\\.]+@([\\\\w-]+\\\\.)+[\\\\w-]{2,4}$/\` but received \\"\\"", - "type": "string", - "types": Object { - "string": "Expected a string matching \`/^[\\\\w-\\\\.]+@([\\\\w-]+\\\\.)+[\\\\w-]{2,4}$/\` but received \\"\\"", + "like": Array [ + Object { + "id": Object { + "message": "Expected a number, but received: \\"z\\"", + "type": "number", + }, + "name": Object { + "message": "Expected a string, but received: undefined", + "type": "string", + }, }, - }, - "enabled": Object { - "message": "Expected a value of type \`boolean\`, but received: \`undefined\`", - "type": "boolean", - "types": Object { - "boolean": "Expected a value of type \`boolean\`, but received: \`undefined\`", - }, - }, + ], "password": Object { "message": "Expected a string matching \`/^[a-zA-Z0-9]{3,30}/\` but received \\"___\\"", "type": "string", - "types": Object { - "string": "Expected a string matching \`/^[a-zA-Z0-9]{3,30}/\` but received \\"___\\"", - }, }, "repeatPassword": Object { "message": "Expected a value of type \`Password\`, but received: \`undefined\`", "type": "Password", - "types": Object { - "Password": "Expected a value of type \`Password\`, but received: \`undefined\`", - }, }, "tags": Object { "message": "Expected an array value, but received: undefined", "type": "array", - "types": Object { - "array": "Expected an array value, but received: undefined", - }, }, "username": Object { "message": "Expected a string, but received: undefined", "type": "string", - "types": Object { - "string": "Expected a string, but received: undefined", - }, }, }, "values": Object {}, diff --git a/superstruct/src/superstruct.ts b/superstruct/src/superstruct.ts index f9d7cb51..bebfd8c4 100644 --- a/superstruct/src/superstruct.ts +++ b/superstruct/src/superstruct.ts @@ -1,62 +1,24 @@ -import { appendErrors } from 'react-hook-form'; import { toNestObject } from '@hookform/resolvers'; +import { FieldError } from 'react-hook-form'; import { StructError, validate } from 'superstruct'; -import { convertArrayToPathName } from '@hookform/resolvers'; import { Resolver } from './types'; -const parseErrorSchema = ( - error: StructError, - validateAllFieldCriteria: boolean, -) => - error - .failures() - .reduce((previous: Record, { path, message = '', type }) => { - const currentPath = convertArrayToPathName(path); - return { - ...previous, - ...(path - ? previous[currentPath] && validateAllFieldCriteria - ? { - [currentPath]: appendErrors( - currentPath, - validateAllFieldCriteria, - previous, - type || '', - message, - ), - } - : { - [currentPath]: previous[currentPath] || { - message, - type, - ...(validateAllFieldCriteria - ? { - types: { [type || '']: message || true }, - } - : {}), - }, - } - : {}), - }; - }, {}); +const parseErrorSchema = (error: StructError) => + error.failures().reduce>( + (previous, error) => + (previous[error.path.join('.')] = { + message: error.message, + type: error.type, + }) && previous, + {}, + ); -export const superstructResolver: Resolver = (schema, options) => async ( - values, - _context, - { criteriaMode }, -) => { - const [errors, result] = validate(values, schema, options); - - if (errors != null) { - return { - values: {}, - errors: toNestObject(parseErrorSchema(errors, criteriaMode === 'all')), - }; - } +export const superstructResolver: Resolver = (schema, options) => (values) => { + const result = validate(values, schema, options); return { - values: result, - errors: {}, + values: result[1] || {}, + errors: result[0] ? toNestObject(parseErrorSchema(result[0])) : {}, }; }; diff --git a/superstruct/src/types.ts b/superstruct/src/types.ts index d125e91d..fc48c577 100644 --- a/superstruct/src/types.ts +++ b/superstruct/src/types.ts @@ -15,4 +15,4 @@ export type Resolver = >( values: UnpackNestedValue, context: TContext | undefined, options: ResolverOptions, -) => Promise>; +) => ResolverResult; From abaec9d537750ca5b256e88bad37aa10b451b4d1 Mon Sep 17 00:00:00 2001 From: jorisre Date: Wed, 3 Feb 2021 11:16:09 +0100 Subject: [PATCH 4/9] feat: add error's ref --- src/__tests__/toNestObject.ts | 21 +++++++++++++++++++-- src/toNestObject.ts | 17 +++++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/__tests__/toNestObject.ts b/src/__tests__/toNestObject.ts index bf6eb226..116cabe1 100644 --- a/src/__tests__/toNestObject.ts +++ b/src/__tests__/toNestObject.ts @@ -1,4 +1,4 @@ -import { FieldError } from 'react-hook-form'; +import { Field, FieldError, InternalFieldName } from 'react-hook-form'; import { toNestObject } from '../toNestObject'; test('transforms flat object to nested object', () => { @@ -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(toNestObject(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/toNestObject.ts b/src/toNestObject.ts index 9b784ded..b1e284d8 100644 --- a/src/toNestObject.ts +++ b/src/toNestObject.ts @@ -1,11 +1,24 @@ -import { set, FieldError, FieldErrors } from 'react-hook-form'; +import { + set, + get, + FieldError, + FieldErrors, + Field, + InternalFieldName, +} from 'react-hook-form'; export const toNestObject = ( errors: Record, + fields: Record, ): FieldErrors => { const fieldErrors: FieldErrors = {}; for (const path in errors) { - set(fieldErrors, path, errors[path]); + const field = get(fields, path) as Field['_f'] | undefined; + set( + fieldErrors, + path, + Object.assign(errors[path], { ref: field && field.ref }), + ); } return fieldErrors; From f54f7f4a37233bdc956e6005b0fbb72252fd6110 Mon Sep 17 00:00:00 2001 From: jorisre Date: Wed, 3 Feb 2021 11:21:23 +0100 Subject: [PATCH 5/9] test: add error's ref + extract fixtures --- joi/src/__tests__/__fixtures__/data.ts | 63 +++++++++ joi/src/__tests__/__snapshots__/joi.ts.snap | 48 +++++++ joi/src/__tests__/joi.ts | 122 ++++-------------- joi/src/joi.ts | 4 +- .../src/__tests__/__fixtures__/data.ts | 65 ++++++++++ .../__snapshots__/superstruct.ts.snap | 26 ++++ superstruct/src/__tests__/superstruct.ts | 66 ++-------- superstruct/src/superstruct.ts | 7 +- vest/src/__tests__/__fixtures__/data.ts | 67 ++++++++++ vest/src/__tests__/__snapshots__/vest.ts.snap | 28 ++++ vest/src/__tests__/vest.ts | 117 ++++------------- vest/src/vest.ts | 7 +- yup/src/__tests__/__fixtures__/data.ts | 52 ++++++++ yup/src/__tests__/__snapshots__/yup.ts.snap | 31 +++++ yup/src/__tests__/yup.ts | 107 ++++----------- yup/src/yup.ts | 4 +- zod/src/__tests__/__fixtures__/data.ts | 20 +++ zod/src/__tests__/__snapshots__/zod.ts.snap | 68 ++++++++++ zod/src/__tests__/zod.ts | 14 +- zod/src/zod.ts | 1 + 20 files changed, 566 insertions(+), 351 deletions(-) create mode 100644 joi/src/__tests__/__fixtures__/data.ts create mode 100644 superstruct/src/__tests__/__fixtures__/data.ts create mode 100644 vest/src/__tests__/__fixtures__/data.ts create mode 100644 yup/src/__tests__/__fixtures__/data.ts diff --git a/joi/src/__tests__/__fixtures__/data.ts b/joi/src/__tests__/__fixtures__/data.ts new file mode 100644 index 00000000..e238df85 --- /dev/null +++ b/joi/src/__tests__/__fixtures__/data.ts @@ -0,0 +1,63 @@ +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(), +}); + +interface Data { + username: string; + password: string; + repeatPassword: string; + accessToken?: number | string; + birthYear?: number; + email?: string; + tags: string[]; + enabled: boolean; +} + +export const validData: Data = { + username: 'Doe', + password: 'Password123', + repeatPassword: 'Password123', + birthYear: 2000, + email: 'john@doe.com', + tags: ['tag1', 'tag2'], + enabled: true, +}; + +export const invalidData = { + password: '___', + email: '', + birthYear: 'birthYear', +}; + +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..0b2f29f2 100644 --- a/joi/src/__tests__/__snapshots__/joi.ts.snap +++ b/joi/src/__tests__/__snapshots__/joi.ts.snap @@ -5,26 +5,38 @@ 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", }, "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 +49,38 @@ 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", }, "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 +93,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 +101,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,6 +111,7 @@ Object { }, "enabled": Object { "message": "\\"enabled\\" is required", + "ref": undefined, "type": "any.required", "types": Object { "any.required": "\\"enabled\\" is required", @@ -90,6 +119,9 @@ Object { }, "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 +129,7 @@ Object { }, "tags": Object { "message": "\\"tags\\" is required", + "ref": undefined, "type": "any.required", "types": Object { "any.required": "\\"tags\\" is required", @@ -104,6 +137,9 @@ Object { }, "username": Object { "message": "\\"username\\" is required", + "ref": Object { + "name": "username", + }, "type": "any.required", "types": Object { "any.required": "\\"username\\" is required", @@ -119,6 +155,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 +163,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,6 +173,7 @@ Object { }, "enabled": Object { "message": "\\"enabled\\" is required", + "ref": undefined, "type": "any.required", "types": Object { "any.required": "\\"enabled\\" is required", @@ -140,6 +181,9 @@ Object { }, "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 +191,7 @@ Object { }, "tags": Object { "message": "\\"tags\\" is required", + "ref": undefined, "type": "any.required", "types": Object { "any.required": "\\"tags\\" is required", @@ -154,6 +199,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..6d1d454b 100644 --- a/joi/src/joi.ts +++ b/joi/src/joi.ts @@ -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: toNestObject(parseErrorSchema(e, criteriaMode === 'all'), fields), }; } }; diff --git a/superstruct/src/__tests__/__fixtures__/data.ts b/superstruct/src/__tests__/__fixtures__/data.ts new file mode 100644 index 00000000..5bc8aecf --- /dev/null +++ b/superstruct/src/__tests__/__fixtures__/data.ts @@ -0,0 +1,65 @@ +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(), +}); + +export const validData: Infer = { + username: 'Doe', + password: 'Password123', + repeatPassword: 'Password123', + birthYear: 2000, + email: 'john@doe.com', + tags: ['tag1', 'tag2'], + enabled: true, +}; + +export const invalidData = { + password: '___', + email: '', + birthYear: 'birthYear', +}; + +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..640f5718 100644 --- a/superstruct/src/__tests__/__snapshots__/superstruct.ts.snap +++ b/superstruct/src/__tests__/__snapshots__/superstruct.ts.snap @@ -5,30 +5,43 @@ 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", }, "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 +54,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 +62,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,6 +72,7 @@ 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\`", @@ -62,6 +80,9 @@ Object { }, "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 +90,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 +98,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 +106,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..d927ba50 100644 --- a/superstruct/src/superstruct.ts +++ b/superstruct/src/superstruct.ts @@ -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: toNestObject( + 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..cb276e21 100644 --- a/vest/src/vest.ts +++ b/vest/src/vest.ts @@ -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: toNestObject( + 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..51010ff3 --- /dev/null +++ b/yup/src/__tests__/__fixtures__/data.ts @@ -0,0 +1,52 @@ +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(), +}); + +export const validData: yup.InferType = { + username: 'Doe', + password: 'Password123', + repeatPassword: 'Password123', + birthYear: 2000, + email: 'john@doe.com', + tags: ['tag1', 'tag2'], + enabled: true, + accessToken: 'accessToken', +}; + +export const invalidData = { + password: '___', + email: '', + birthYear: 'birthYear', +}; + +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..78cbaa14 100644 --- a/yup/src/__tests__/__snapshots__/yup.ts.snap +++ b/yup/src/__tests__/__snapshots__/yup.ts.snap @@ -5,14 +5,21 @@ 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", }, "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 +32,21 @@ 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", }, "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,6 +59,7 @@ 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\\"\`).", @@ -52,6 +67,9 @@ Object { }, "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 +77,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,6 +95,7 @@ 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\\"\`).", @@ -81,6 +103,9 @@ Object { }, "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 +113,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 +131,7 @@ Object { "errors": Object { "name": Object { "message": "name must be at least 6 characters", + "ref": undefined, "type": "min", }, }, @@ -115,6 +144,7 @@ Object { "errors": Object { "required": Object { "message": "error1", + "ref": undefined, "type": "required", }, }, @@ -127,6 +157,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..363be82e 100644 --- a/yup/src/yup.ts +++ b/yup/src/yup.ts @@ -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: toNestObject(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..861fb7e5 100644 --- a/zod/src/zod.ts +++ b/zod/src/zod.ts @@ -58,6 +58,7 @@ export const zodResolver: Resolver = ( ? {} : toNestObject( parseErrorSchema(error.errors, options.criteriaMode === 'all'), + options.fields, ), }; } From 14aa81cb0b9fff2ec947db87042aaa0960e719bf Mon Sep 17 00:00:00 2001 From: jorisre Date: Wed, 3 Feb 2021 13:37:35 +0100 Subject: [PATCH 6/9] test: add nested test --- joi/src/__tests__/__fixtures__/data.ts | 11 +++ joi/src/__tests__/__snapshots__/joi.ts.snap | 42 ++++++++++++ .../src/__tests__/__fixtures__/data.ts | 8 +++ .../__snapshots__/superstruct.ts.snap | 34 ++++++++++ yup/src/__tests__/__fixtures__/data.ts | 13 ++++ yup/src/__tests__/__snapshots__/yup.ts.snap | 68 +++++++++++++++++++ 6 files changed, 176 insertions(+) diff --git a/joi/src/__tests__/__fixtures__/data.ts b/joi/src/__tests__/__fixtures__/data.ts index e238df85..3404baf0 100644 --- a/joi/src/__tests__/__fixtures__/data.ts +++ b/joi/src/__tests__/__fixtures__/data.ts @@ -14,6 +14,9 @@ export const schema = Joi.object({ }), 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 { @@ -25,6 +28,7 @@ interface Data { email?: string; tags: string[]; enabled: boolean; + like: { id: number; name: string }[]; } export const validData: Data = { @@ -35,12 +39,19 @@ export const validData: Data = { 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 = { diff --git a/joi/src/__tests__/__snapshots__/joi.ts.snap b/joi/src/__tests__/__snapshots__/joi.ts.snap index 0b2f29f2..7277cb47 100644 --- a/joi/src/__tests__/__snapshots__/joi.ts.snap +++ b/joi/src/__tests__/__snapshots__/joi.ts.snap @@ -20,6 +20,15 @@ Object { "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 { @@ -64,6 +73,15 @@ Object { "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 { @@ -117,6 +135,18 @@ 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 { @@ -179,6 +209,18 @@ 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 { diff --git a/superstruct/src/__tests__/__fixtures__/data.ts b/superstruct/src/__tests__/__fixtures__/data.ts index 5bc8aecf..c69d51fc 100644 --- a/superstruct/src/__tests__/__fixtures__/data.ts +++ b/superstruct/src/__tests__/__fixtures__/data.ts @@ -27,6 +27,7 @@ export const schema = object({ 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 = { @@ -37,12 +38,19 @@ export const validData: Infer = { 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 = { diff --git a/superstruct/src/__tests__/__snapshots__/superstruct.ts.snap b/superstruct/src/__tests__/__snapshots__/superstruct.ts.snap index 640f5718..f556a4f3 100644 --- a/superstruct/src/__tests__/__snapshots__/superstruct.ts.snap +++ b/superstruct/src/__tests__/__snapshots__/superstruct.ts.snap @@ -20,6 +20,20 @@ Object { "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 { @@ -78,6 +92,26 @@ 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 { diff --git a/yup/src/__tests__/__fixtures__/data.ts b/yup/src/__tests__/__fixtures__/data.ts index 51010ff3..9499b94a 100644 --- a/yup/src/__tests__/__fixtures__/data.ts +++ b/yup/src/__tests__/__fixtures__/data.ts @@ -13,6 +13,12 @@ export const schema = yup.object({ 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 = { @@ -24,12 +30,19 @@ export const validData: yup.InferType = { 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 = { diff --git a/yup/src/__tests__/__snapshots__/yup.ts.snap b/yup/src/__tests__/__snapshots__/yup.ts.snap index 78cbaa14..5e8da723 100644 --- a/yup/src/__tests__/__snapshots__/yup.ts.snap +++ b/yup/src/__tests__/__snapshots__/yup.ts.snap @@ -8,6 +8,20 @@ Object { "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 { @@ -35,6 +49,20 @@ Object { "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 { @@ -65,6 +93,26 @@ 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 { @@ -101,6 +149,26 @@ 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 { From 8b4dc434f8b387dc2175f52ec5c8ca881b798a5e Mon Sep 17 00:00:00 2001 From: jorisre Date: Wed, 3 Feb 2021 13:44:33 +0100 Subject: [PATCH 7/9] refactor: rename toNestObject to toNestError --- joi/src/joi.ts | 4 ++-- src/__tests__/toNestObject.ts | 4 ++-- src/index.ts | 2 +- src/{toNestObject.ts => toNestError.ts} | 2 +- superstruct/src/superstruct.ts | 4 ++-- vest/src/vest.ts | 4 ++-- yup/src/yup.ts | 4 ++-- zod/src/zod.ts | 4 ++-- 8 files changed, 14 insertions(+), 14 deletions(-) rename src/{toNestObject.ts => toNestError.ts} (92%) diff --git a/joi/src/joi.ts b/joi/src/joi.ts index 6d1d454b..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'; @@ -78,7 +78,7 @@ export const joiResolver: Resolver = ( } catch (e) { return { values: {}, - errors: toNestObject(parseErrorSchema(e, criteriaMode === 'all'), fields), + errors: toNestError(parseErrorSchema(e, criteriaMode === 'all'), fields), }; } }; diff --git a/src/__tests__/toNestObject.ts b/src/__tests__/toNestObject.ts index 116cabe1..877eaeac 100644 --- a/src/__tests__/toNestObject.ts +++ b/src/__tests__/toNestObject.ts @@ -1,5 +1,5 @@ import { Field, FieldError, InternalFieldName } from 'react-hook-form'; -import { toNestObject } from '../toNestObject'; +import { toNestError } from '../toNestError'; test('transforms flat object to nested object', () => { const flatObject: Record = { @@ -22,7 +22,7 @@ test('transforms flat object to nested object', () => { }, } as any) as Record; - expect(toNestObject(flatObject, fields)).toMatchInlineSnapshot(` + expect(toNestError(flatObject, fields)).toMatchInlineSnapshot(` Object { "n": Object { "test": Object { 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/toNestObject.ts b/src/toNestError.ts similarity index 92% rename from src/toNestObject.ts rename to src/toNestError.ts index b1e284d8..72672aed 100644 --- a/src/toNestObject.ts +++ b/src/toNestError.ts @@ -7,7 +7,7 @@ import { InternalFieldName, } from 'react-hook-form'; -export const toNestObject = ( +export const toNestError = ( errors: Record, fields: Record, ): FieldErrors => { diff --git a/superstruct/src/superstruct.ts b/superstruct/src/superstruct.ts index d927ba50..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'; @@ -51,7 +51,7 @@ export const superstructResolver: Resolver = (schema, options) => async ( if (errors != null) { return { values: {}, - errors: toNestObject( + errors: toNestError( parseErrorSchema(errors, criteriaMode === 'all'), fields, ), diff --git a/vest/src/vest.ts b/vest/src/vest.ts index cb276e21..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'; @@ -49,7 +49,7 @@ export const vestResolver: Resolver = ( return { values: {}, - errors: toNestObject( + errors: toNestError( parseErrorSchema(errors, criteriaMode === 'all'), fields, ), diff --git a/yup/src/yup.ts b/yup/src/yup.ts index 363be82e..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'; /** @@ -89,7 +89,7 @@ export const yupResolver: Resolver = ( return { values: {}, - errors: toNestObject(parsedErrors, fields), + errors: toNestError(parsedErrors, fields), }; } }; diff --git a/zod/src/zod.ts b/zod/src/zod.ts index 861fb7e5..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,7 +56,7 @@ export const zodResolver: Resolver = ( values: {}, errors: error.isEmpty ? {} - : toNestObject( + : toNestError( parseErrorSchema(error.errors, options.criteriaMode === 'all'), options.fields, ), From f63e3fe121735ce286bdd99b44ddeb1eab5f0149 Mon Sep 17 00:00:00 2001 From: jorisre Date: Wed, 3 Feb 2021 14:09:38 +0100 Subject: [PATCH 8/9] refactor: remove duplicate line --- jest.config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index f09cc240..24fe9d3b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,7 +3,6 @@ module.exports = { testEnvironment: 'jsdom', restoreMocks: true, testMatch: ['**/__tests__/**/*.+(js|jsx|ts|tsx)'], - testPathIgnorePatterns: ['/__fixtures__/'], transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$'], testPathIgnorePatterns: ['/__fixtures__/'], moduleNameMapper: { From d08aa67ab99358ff0fda1cc4f329f830995c66e2 Mon Sep 17 00:00:00 2001 From: jorisre Date: Wed, 3 Feb 2021 22:55:01 +0100 Subject: [PATCH 9/9] chore: update superstruct peer dep --- superstruct/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superstruct/package.json b/superstruct/package.json index f42ea659..8065d6b2 100644 --- a/superstruct/package.json +++ b/superstruct/package.json @@ -13,6 +13,6 @@ "peerDependencies": { "react-hook-form": ">=6.6.0", "@hookform/resolvers": "^1.0.0", - "superstruct": ">+0.12.0" + "superstruct": ">=0.12.0" } }