From e0576db3c21a432dfe0355d7e65e7cae835d09ed Mon Sep 17 00:00:00 2001 From: Joris Date: Thu, 30 Jan 2025 12:09:27 +0100 Subject: [PATCH] feat(effectResolver): returns either all errors or only the first one based on criteriaMode --- .../__tests__/__snapshots__/effect-ts.ts.snap | 36 ++++++++++- effect-ts/src/__tests__/effect-ts.ts | 61 +++++++++++++++++++ effect-ts/src/effect-ts.ts | 37 +++++++++-- 3 files changed, 127 insertions(+), 7 deletions(-) diff --git a/effect-ts/src/__tests__/__snapshots__/effect-ts.ts.snap b/effect-ts/src/__tests__/__snapshots__/effect-ts.ts.snap index 6d97f0e1..0e9e0cc6 100644 --- a/effect-ts/src/__tests__/__snapshots__/effect-ts.ts.snap +++ b/effect-ts/src/__tests__/__snapshots__/effect-ts.ts.snap @@ -4,7 +4,7 @@ exports[`effectTsResolver > should return a single error from effectTsResolver w { "errors": { "animal": { - "message": "Expected "snake", actual ["dog"]", + "message": "Expected string, actual ["dog"]", "ref": undefined, "type": "Type", }, @@ -38,3 +38,37 @@ exports[`effectTsResolver > should return a single error from effectTsResolver w "values": {}, } `; + +exports[`effectTsResolver > should return all the errors from effectTsResolver when validation fails with \`validateAllFieldCriteria\` set to true 1`] = ` +{ + "errors": { + "phoneNumber": { + "message": "Please enter a valid phone number in international format.", + "ref": { + "name": "phoneNumber", + }, + "type": "Refinement", + "types": { + "Refinement": "Please enter a valid phone number in international format.", + "Type": "Expected undefined, actual "123"", + }, + }, + }, + "values": {}, +} +`; + +exports[`effectTsResolver > should return the first error from effectTsResolver when validation fails with \`validateAllFieldCriteria\` set to firstError 1`] = ` +{ + "errors": { + "phoneNumber": { + "message": "Please enter a valid phone number in international format.", + "ref": { + "name": "phoneNumber", + }, + "type": "Refinement", + }, + }, + "values": {}, +} +`; diff --git a/effect-ts/src/__tests__/effect-ts.ts b/effect-ts/src/__tests__/effect-ts.ts index 409adacd..cfe296be 100644 --- a/effect-ts/src/__tests__/effect-ts.ts +++ b/effect-ts/src/__tests__/effect-ts.ts @@ -1,3 +1,4 @@ +import { Schema } from 'effect'; import { effectTsResolver } from '..'; import { fields, invalidData, schema, validData } from './__fixtures__/data'; @@ -21,4 +22,64 @@ describe('effectTsResolver', () => { expect(result).toMatchSnapshot(); }); + + it('should return the first error from effectTsResolver when validation fails with `validateAllFieldCriteria` set to firstError', async () => { + const SignupSchema = Schema.Struct({ + phoneNumber: Schema.optional( + Schema.String.pipe( + Schema.pattern(/^\+\d{7,15}$/, { + message: () => + 'Please enter a valid phone number in international format.', + }), + ), + ), + }); + + const result = await effectTsResolver(SignupSchema)( + { phoneNumber: '123' }, + undefined, + { + fields: { + phoneNumber: { + ref: { name: 'phoneNumber' }, + name: 'phoneNumber', + }, + }, + criteriaMode: 'firstError', + shouldUseNativeValidation, + }, + ); + + expect(result).toMatchSnapshot(); + }); + + it('should return all the errors from effectTsResolver when validation fails with `validateAllFieldCriteria` set to true', async () => { + const SignupSchema = Schema.Struct({ + phoneNumber: Schema.optional( + Schema.String.pipe( + Schema.pattern(/^\+\d{7,15}$/, { + message: () => + 'Please enter a valid phone number in international format.', + }), + ), + ), + }); + + const result = await effectTsResolver(SignupSchema)( + { phoneNumber: '123' }, + undefined, + { + fields: { + phoneNumber: { + ref: { name: 'phoneNumber' }, + name: 'phoneNumber', + }, + }, + criteriaMode: 'all', + shouldUseNativeValidation, + }, + ); + + expect(result).toMatchSnapshot(); + }); }); diff --git a/effect-ts/src/effect-ts.ts b/effect-ts/src/effect-ts.ts index f3b8ba70..b99a7584 100644 --- a/effect-ts/src/effect-ts.ts +++ b/effect-ts/src/effect-ts.ts @@ -2,7 +2,7 @@ import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers'; import { Effect } from 'effect'; import { ArrayFormatter, decodeUnknown } from 'effect/ParseResult'; -import type { FieldErrors } from 'react-hook-form'; +import { appendErrors, type FieldError } from 'react-hook-form'; import type { Resolver } from './types'; export const effectTsResolver: Resolver = @@ -16,11 +16,36 @@ export const effectTsResolver: Resolver = Effect.flip(ArrayFormatter.formatIssue(parseIssue)), ), Effect.mapError((issues) => { - const errors = issues.reduce((acc, current) => { - const key = current.path.join('.'); - acc[key] = { message: current.message, type: current._tag }; - return acc; - }, {} as FieldErrors); + const validateAllFieldCriteria = + !options.shouldUseNativeValidation && options.criteriaMode === 'all'; + + const errors = issues.reduce( + (acc, error) => { + const key = error.path.join('.'); + + if (!acc[key]) { + acc[key] = { message: error.message, type: error._tag }; + } + + if (validateAllFieldCriteria) { + const types = acc[key].types; + const messages = types && types[String(error._tag)]; + + acc[key] = appendErrors( + key, + validateAllFieldCriteria, + acc, + error._tag, + messages + ? ([] as string[]).concat(messages as string[], error.message) + : error.message, + ) as FieldError; + } + + return acc; + }, + {} as Record, + ); return toNestErrors(errors, options); }),