From 97f3ed3593d4ac225e811b006aa2aec7582f4627 Mon Sep 17 00:00:00 2001 From: jorisre Date: Tue, 2 Feb 2021 14:21:48 +0100 Subject: [PATCH 1/2] test: extract fixtures --- jest.config.js | 1 + zod/src/__tests__/__fixtures__/data.ts | 48 +++++++++++++ zod/src/__tests__/zod.ts | 94 +++++--------------------- 3 files changed, 66 insertions(+), 77 deletions(-) create mode 100644 zod/src/__tests__/__fixtures__/data.ts diff --git a/jest.config.js b/jest.config.js index 0445b64d..24fe9d3b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,6 +4,7 @@ module.exports = { restoreMocks: true, testMatch: ['**/__tests__/**/*.+(js|jsx|ts|tsx)'], transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$'], + testPathIgnorePatterns: ['/__fixtures__/'], moduleNameMapper: { '^@hookform/resolvers$': '/src', }, diff --git a/zod/src/__tests__/__fixtures__/data.ts b/zod/src/__tests__/__fixtures__/data.ts new file mode 100644 index 00000000..0c2f5f83 --- /dev/null +++ b/zod/src/__tests__/__fixtures__/data.ts @@ -0,0 +1,48 @@ +import * as z from 'zod'; + +export const schema = z + .object({ + username: z.string().regex(/^\w+$/).min(3).max(30), + password: z.string().regex(/^[a-zA-Z0-9]{3,30}/), + repeatPassword: z.string(), + accessToken: z.union([z.string(), z.number()]).optional(), + birthYear: z.number().min(1900).max(2013).optional(), + email: z.string().email().optional(), + tags: z.array(z.string()), + enabled: z.boolean(), + like: z + .array( + z.object({ + id: z.number(), + name: z.string().length(4), + }), + ) + .optional(), + }) + .refine((data) => data.password === data.repeatPassword, { + message: "Passwords don't match", + path: ['confirm'], // set path of error + }); + +export const validData: z.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/zod/src/__tests__/zod.ts b/zod/src/__tests__/zod.ts index c9e4686d..bf2cda92 100644 --- a/zod/src/__tests__/zod.ts +++ b/zod/src/__tests__/zod.ts @@ -1,94 +1,46 @@ -import * as z from 'zod'; import { zodResolver } from '..'; - -const schema = z - .object({ - username: z.string().regex(/^\w+$/).min(3).max(30), - password: z.string().regex(/^[a-zA-Z0-9]{3,30}/), - repeatPassword: z.string(), - accessToken: z.union([z.string(), z.number()]).optional(), - birthYear: z.number().min(1900).max(2013).optional(), - email: z.string().email().optional(), - tags: z.array(z.string()), - enabled: z.boolean(), - }) - .refine((data) => data.password === data.repeatPassword, { - message: "Passwords don't match", - path: ['confirm'], // set path of error - }); +import { schema, validData, invalidData } from './__fixtures__/data'; describe('zodResolver', () => { it('should return values from zodResolver when validation pass', async () => { - const data: z.infer = { - username: 'Doe', - password: 'Password123', - repeatPassword: 'Password123', - birthYear: 2000, - email: 'john@doe.com', - tags: ['tag1', 'tag2'], - enabled: true, - }; - const parseAsyncSpy = jest.spyOn(schema, 'parseAsync'); - const result = await zodResolver(schema)(data, undefined, { fields: {} }); + const result = await zodResolver(schema)(validData, undefined, { + fields: {}, + }); expect(parseAsyncSpy).toHaveBeenCalledTimes(1); - expect(result).toEqual({ errors: {}, values: data }); + expect(result).toEqual({ errors: {}, values: validData }); }); it('should return values from zodResolver with `mode: sync` when validation pass', async () => { - const data: z.infer = { - username: 'Doe', - password: 'Password123', - repeatPassword: 'Password123', - birthYear: 2000, - email: 'john@doe.com', - tags: ['tag1', 'tag2'], - enabled: true, - }; - const parseSpy = jest.spyOn(schema, 'parse'); const parseAsyncSpy = jest.spyOn(schema, 'parseAsync'); - const result = await zodResolver(schema, undefined, { mode: 'sync' })( - data, - undefined, - { fields: {} }, - ); + const result = await zodResolver(schema, undefined, { + mode: 'sync', + })(validData, undefined, { fields: {} }); expect(parseSpy).toHaveBeenCalledTimes(1); expect(parseAsyncSpy).not.toHaveBeenCalled(); - expect(result).toEqual({ errors: {}, values: data }); + expect(result).toEqual({ errors: {}, values: validData }); }); it('should return a single error from zodResolver when validation fails', async () => { - const data = { - password: '___', - email: '', - birthYear: 'birthYear', - }; - - const result = await zodResolver(schema)(data, undefined, { fields: {} }); + const result = await zodResolver(schema)(invalidData, undefined, { + fields: {}, + }); expect(result).toMatchSnapshot(); }); it('should return a single error from zodResolver with `mode: sync` when validation fails', async () => { - const data = { - password: '___', - email: '', - birthYear: 'birthYear', - }; - const parseSpy = jest.spyOn(schema, 'parse'); const parseAsyncSpy = jest.spyOn(schema, 'parseAsync'); - const result = await zodResolver(schema, undefined, { mode: 'sync' })( - data, - undefined, - { fields: {} }, - ); + const result = await zodResolver(schema, undefined, { + mode: 'sync', + })(invalidData, undefined, { fields: {} }); expect(parseSpy).toHaveBeenCalledTimes(1); expect(parseAsyncSpy).not.toHaveBeenCalled(); @@ -96,13 +48,7 @@ describe('zodResolver', () => { }); it('should return all the errors from zodResolver when validation fails with `validateAllFieldCriteria` set to true', async () => { - const data = { - password: '___', - email: '', - birthYear: 'birthYear', - }; - - const result = await zodResolver(schema)(data, undefined, { + const result = await zodResolver(schema)(invalidData, undefined, { fields: {}, criteriaMode: 'all', }); @@ -111,14 +57,8 @@ describe('zodResolver', () => { }); it('should return all the errors from zodResolver when validation fails with `validateAllFieldCriteria` set to true and `mode: sync`', async () => { - const data = { - password: '___', - email: '', - birthYear: 'birthYear', - }; - const result = await zodResolver(schema, undefined, { mode: 'sync' })( - data, + invalidData, undefined, { fields: {}, From c498a024e22b1a59b17d52ef4bf4fa41edeadbe2 Mon Sep 17 00:00:00 2001 From: jorisre Date: Tue, 2 Feb 2021 14:43:55 +0100 Subject: [PATCH 2/2] fix: zod resolver union error + reduce resolver size --- zod/src/__tests__/__snapshots__/zod.ts.snap | 78 ++++++++++++++++++ zod/src/types.ts | 2 +- zod/src/zod.ts | 91 ++++++++++----------- 3 files changed, 123 insertions(+), 48 deletions(-) diff --git a/zod/src/__tests__/__snapshots__/zod.ts.snap b/zod/src/__tests__/__snapshots__/zod.ts.snap index c3df61a5..b7134ac1 100644 --- a/zod/src/__tests__/__snapshots__/zod.ts.snap +++ b/zod/src/__tests__/__snapshots__/zod.ts.snap @@ -19,6 +19,20 @@ Object { "message": "Required", "type": "invalid_type", }, + "like": Object { + "0": Object { + "id": Object { + "message": "Expected number, received string", + "type": "invalid_type", + }, + "name": Object { + "message": "Required", + "type": "invalid_type", + }, + }, + "message": "Invalid input", + "type": "invalid_union", + }, "password": Object { "message": "Invalid", "type": "invalid_string", @@ -59,6 +73,20 @@ Object { "message": "Required", "type": "invalid_type", }, + "like": Object { + "0": Object { + "id": Object { + "message": "Expected number, received string", + "type": "invalid_type", + }, + "name": Object { + "message": "Required", + "type": "invalid_type", + }, + }, + "message": "Invalid input", + "type": "invalid_union", + }, "password": Object { "message": "Invalid", "type": "invalid_string", @@ -87,6 +115,7 @@ Object { "message": "Invalid input", "type": "invalid_union", "types": Object { + "invalid_type": "Expected undefined, received string", "invalid_union": "Invalid input", }, }, @@ -111,6 +140,30 @@ Object { "invalid_type": "Required", }, }, + "like": Object { + "0": Object { + "id": Object { + "message": "Expected number, received string", + "type": "invalid_type", + "types": Object { + "invalid_type": "Expected number, received string", + }, + }, + "name": Object { + "message": "Required", + "type": "invalid_type", + "types": Object { + "invalid_type": "Required", + }, + }, + }, + "message": "Invalid input", + "type": "invalid_union", + "types": Object { + "invalid_type": "Expected undefined, received array", + "invalid_union": "Invalid input", + }, + }, "password": Object { "message": "Invalid", "type": "invalid_string", @@ -151,6 +204,7 @@ Object { "message": "Invalid input", "type": "invalid_union", "types": Object { + "invalid_type": "Expected undefined, received string", "invalid_union": "Invalid input", }, }, @@ -175,6 +229,30 @@ Object { "invalid_type": "Required", }, }, + "like": Object { + "0": Object { + "id": Object { + "message": "Expected number, received string", + "type": "invalid_type", + "types": Object { + "invalid_type": "Expected number, received string", + }, + }, + "name": Object { + "message": "Required", + "type": "invalid_type", + "types": Object { + "invalid_type": "Required", + }, + }, + }, + "message": "Invalid input", + "type": "invalid_union", + "types": Object { + "invalid_type": "Expected undefined, received array", + "invalid_union": "Invalid input", + }, + }, "password": Object { "message": "Invalid", "type": "invalid_string", diff --git a/zod/src/types.ts b/zod/src/types.ts index 3126ba03..dd923848 100644 --- a/zod/src/types.ts +++ b/zod/src/types.ts @@ -10,7 +10,7 @@ import type { ParseParams } from 'zod/lib/src/parser'; export type Resolver = >( schema: T, schemaOptions?: ParseParams, - factoryOptions?: { mode: 'async' | 'sync' }, + factoryOptions?: { mode?: 'async' | 'sync' }, ) => ( values: UnpackNestedValue, context: TContext | undefined, diff --git a/zod/src/zod.ts b/zod/src/zod.ts index 2ced1b83..1f476973 100644 --- a/zod/src/zod.ts +++ b/zod/src/zod.ts @@ -1,67 +1,64 @@ -import { appendErrors } from 'react-hook-form'; +import { appendErrors, FieldError } from 'react-hook-form'; import * as z from 'zod'; -import { convertArrayToPathName, toNestObject } from '@hookform/resolvers'; +import { toNestObject } from '@hookform/resolvers'; import type { Resolver } from './types'; const parseErrorSchema = ( - zodError: z.ZodError, + zodErrors: z.ZodSuberror[], validateAllFieldCriteria: boolean, ) => { - if (zodError.isEmpty) { - return {}; - } + const errors: Record = {}; + for (; zodErrors.length; ) { + const error = zodErrors[0]; + const { code, message, path } = error; + const _path = path.join('.'); + + if (!errors[_path]) { + errors[_path] = { message, type: code }; + } + + if ('unionErrors' in error) { + error.unionErrors.forEach((unionError) => + unionError.errors.forEach((e) => zodErrors.push(e)), + ); + } + + if (validateAllFieldCriteria) { + errors[_path] = appendErrors( + _path, + validateAllFieldCriteria, + errors, + code, + message, + ) as FieldError; + } - return zodError.errors.reduce>( - (previous, { path, message, code: type }) => { - const currentPath = convertArrayToPathName(path); + zodErrors.shift(); + } - return { - ...previous, - ...(path - ? previous[currentPath] && validateAllFieldCriteria - ? { - [currentPath]: appendErrors( - currentPath, - validateAllFieldCriteria, - previous, - type, - message, - ), - } - : { - [currentPath]: previous[currentPath] || { - message, - type, - ...(validateAllFieldCriteria - ? { - types: { [type]: message || true }, - } - : {}), - }, - } - : {}), - }; - }, - {}, - ); + return errors; }; export const zodResolver: Resolver = ( schema, schemaOptions, - { mode } = { mode: 'async' }, -) => async (values, _, { criteriaMode }) => { + resolverOptions = {}, +) => async (values, _, options) => { try { - const result = - mode === 'async' - ? await schema.parseAsync(values, schemaOptions) - : schema.parse(values, schemaOptions); - - return { values: result, errors: {} }; + return { + errors: {}, + values: await schema[ + resolverOptions.mode === 'sync' ? 'parse' : 'parseAsync' + ](values, schemaOptions), + }; } catch (error) { return { values: {}, - errors: toNestObject(parseErrorSchema(error, criteriaMode === 'all')), + errors: error.isEmpty + ? {} + : toNestObject( + parseErrorSchema(error.errors, options.criteriaMode === 'all'), + ), }; } };