Skip to content

Commit

Permalink
feat: add support to async functions on server validate (#855)
Browse files Browse the repository at this point in the history
* serverValidate `validate` to `validateAsync`

* Fix `onServerValidate` type

* `pnpm run prettier:write`

* Add createServerValidate test for zod-form-adapter

* test: add tests on yup and valibot

---------

Co-authored-by: Leonardo Montini <[email protected]>
  • Loading branch information
ogawa0071 and Balastrong authored Aug 27, 2024
1 parent 179a323 commit 4e7f01d
Show file tree
Hide file tree
Showing 9 changed files with 526 additions and 153 deletions.
18 changes: 13 additions & 5 deletions packages/react-form/src/nextjs/createServerValidate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,21 @@ import type { ServerFormState } from './types'

type OnServerValidateFn<TFormData> = (props: {
value: TFormData
}) => ValidationError
}) => ValidationError | Promise<ValidationError>

type OnServerValidateOrFn<
TFormData,
TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
> =
TFormValidator extends Validator<TFormData, infer FFN>
? FFN | OnServerValidateFn<TFormData>
: OnServerValidateFn<TFormData>

interface CreateServerValidateOptions<
TFormData,
TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
> extends FormOptions<TFormData, TFormValidator> {
onServerValidate: OnServerValidateFn<TFormData>
onServerValidate: OnServerValidateOrFn<TFormData, TFormValidator>
}

export const createServerValidate =
Expand All @@ -30,17 +38,17 @@ export const createServerValidate =
async (formData: FormData, info?: Parameters<typeof decode>[1]) => {
const { validatorAdapter, onServerValidate } = defaultOpts

const runValidator = (propsValue: { value: TFormData }) => {
const runValidator = async (propsValue: { value: TFormData }) => {
if (validatorAdapter && typeof onServerValidate !== 'function') {
return validatorAdapter().validate(propsValue, onServerValidate)
return validatorAdapter().validateAsync(propsValue, onServerValidate)
}

return (onServerValidate as OnServerValidateFn<TFormData>)(propsValue)
}

const values = decode(formData, info) as never as TFormData

const onServerError = runValidator({ value: values })
const onServerError = await runValidator({ value: values })

if (!onServerError) return

Expand Down
18 changes: 13 additions & 5 deletions packages/react-form/src/start/createServerValidate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,21 @@ type Ctx = Parameters<FetchFn<FormData, unknown>>[1]

type OnServerValidateFn<TFormData> = (props: {
value: TFormData
}) => ValidationError
}) => ValidationError | Promise<ValidationError>

type OnServerValidateOrFn<
TFormData,
TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
> =
TFormValidator extends Validator<TFormData, infer FFN>
? FFN | OnServerValidateFn<TFormData>
: OnServerValidateFn<TFormData>

interface CreateServerValidateOptions<
TFormData,
TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
> extends FormOptions<TFormData, TFormValidator> {
onServerValidate: OnServerValidateFn<TFormData>
onServerValidate: OnServerValidateOrFn<TFormData, TFormValidator>
}

export const createServerValidate =
Expand All @@ -34,9 +42,9 @@ export const createServerValidate =
async (ctx: Ctx, formData: FormData, info?: Parameters<typeof decode>[1]) => {
const { validatorAdapter, onServerValidate } = defaultOpts

const runValidator = (propsValue: { value: TFormData }) => {
const runValidator = async (propsValue: { value: TFormData }) => {
if (validatorAdapter && typeof onServerValidate !== 'function') {
return validatorAdapter().validate(propsValue, onServerValidate)
return validatorAdapter().validateAsync(propsValue, onServerValidate)
}

return (onServerValidate as OnServerValidateFn<TFormData>)(propsValue)
Expand All @@ -46,7 +54,7 @@ export const createServerValidate =

const data = decode(formData, info) as never as TFormData

const onServerError = runValidator({ value: data })
const onServerError = await runValidator({ value: data })

if (!onServerError) return

Expand Down
1 change: 1 addition & 0 deletions packages/valibot-form-adapter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@tanstack/form-core": "workspace:*"
},
"devDependencies": {
"@tanstack/react-form": "workspace:*",
"valibot": "^0.39.0"
},
"peerDependencies": {
Expand Down
118 changes: 118 additions & 0 deletions packages/valibot-form-adapter/tests/createServerValidate.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { describe, expect, it } from 'vitest'

import { createServerValidate } from '@tanstack/react-form/nextjs'
import * as v from 'valibot'
import { valibotValidator } from '../src/index'
import { sleep } from './utils'

describe('valibot createServerValidate api', () => {
it('should run v.string validation', async () => {
const serverValidate = createServerValidate({
validatorAdapter: valibotValidator(),
onServerValidate: v.object({
name: v.pipe(
v.string(),
v.minLength(3, 'You must have a length of at least 3'),
),
}),
})

const formData1 = new FormData()
formData1.append('name', 'a')
expect(
await serverValidate(formData1).catch((e) => e.formState.errors),
).toEqual(['You must have a length of at least 3'])

const formData2 = new FormData()
formData2.append('name', 'asdf')
expect(await serverValidate(formData2)).toBeUndefined()
})

it('should run fn with valibot validation option enabled', async () => {
const serverValidate = createServerValidate({
validatorAdapter: valibotValidator(),
onServerValidate: ({ value }: { value: { name: string } }) =>
value.name === 'a' ? 'Test' : undefined,
})

const formData1 = new FormData()
formData1.append('name', 'a')
expect(
await serverValidate(formData1).catch((e) => e.formState.errors),
).toEqual(['Test'])

const formData2 = new FormData()
formData2.append('name', 'asdf')
expect(await serverValidate(formData2)).toBeUndefined()
})

it('should run v.string async validation', async () => {
const serverValidate = createServerValidate({
validatorAdapter: valibotValidator(),
onServerValidate: v.pipeAsync(
v.object({
name: v.string(),
}),
v.checkAsync(async (val) => {
await sleep(1)
return val.name.length > 3
}, 'Testing 123'),
),
})

const formData1 = new FormData()
formData1.append('name', 'a')
expect(
await serverValidate(formData1).catch((e) => e.formState.errors),
).toEqual(['Testing 123'])

const formData2 = new FormData()
formData2.append('name', 'asdf')
expect(await serverValidate(formData2)).toBeUndefined()
})

it('should run async fn with valibot validation option enabled', async () => {
const serverValidate = createServerValidate({
validatorAdapter: valibotValidator(),
onServerValidate: async ({ value }: { value: { name: string } }) => {
await sleep(1)
return value.name === 'a' ? 'Test' : undefined
},
})

const formData1 = new FormData()
formData1.append('name', 'a')
expect(
await serverValidate(formData1).catch((e) => e.formState.errors),
).toEqual(['Test'])

const formData2 = new FormData()
formData2.append('name', 'asdf')
expect(await serverValidate(formData2)).toBeUndefined()
})

it('should transform errors to display all error message', async () => {
const serverValidate = createServerValidate({
validatorAdapter: valibotValidator(),
onServerValidate: v.object({
name: v.pipe(
v.string(),
v.minLength(3, 'You must have a length of at least 3'),
v.uuid('UUID'),
),
}),
})

const formData1 = new FormData()
formData1.append('name', 'aa')
expect(
await serverValidate(formData1).catch((e) => e.formState.errors),
).toEqual(['You must have a length of at least 3, UUID'])

const formData2 = new FormData()
formData2.append('name', 'aaa')
expect(
await serverValidate(formData2).catch((e) => e.formState.errors),
).toEqual(['UUID'])
})
})
3 changes: 2 additions & 1 deletion packages/yup-form-adapter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
"@tanstack/form-core": "workspace:*"
},
"devDependencies": {
"yup": "^1.4.0"
"yup": "^1.4.0",
"@tanstack/react-form": "workspace:*"
},
"peerDependencies": {
"yup": "^1.x"
Expand Down
111 changes: 111 additions & 0 deletions packages/yup-form-adapter/tests/createServerValidate.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { describe, expect, it } from 'vitest'

import { createServerValidate } from '@tanstack/react-form/nextjs'
import yup from 'yup'
import { yupValidator } from '../src/index'
import { sleep } from './utils'

describe('yup createServerValidate api', () => {
it('should run yup.string validation', async () => {
const serverValidate = createServerValidate({
validatorAdapter: yupValidator(),
onServerValidate: yup.object({
name: yup.string().min(3, 'You must have a length of at least 3'),
}),
})

const formData1 = new FormData()
formData1.append('name', 'a')
expect(
await serverValidate(formData1).catch((e) => e.formState.errors),
).toEqual(['You must have a length of at least 3'])

const formData2 = new FormData()
formData2.append('name', 'asdf')
expect(await serverValidate(formData2)).toBeUndefined()
})

it('should run fn with yup validation option enabled', async () => {
const serverValidate = createServerValidate({
validatorAdapter: yupValidator(),
onServerValidate: ({ value }: { value: { name: string } }) =>
value.name === 'a' ? 'Test' : undefined,
})

const formData1 = new FormData()
formData1.append('name', 'a')
expect(
await serverValidate(formData1).catch((e) => e.formState.errors),
).toEqual(['Test'])

const formData2 = new FormData()
formData2.append('name', 'asdf')
expect(await serverValidate(formData2)).toBeUndefined()
})

it('should run yup.string async validation', async () => {
const serverValidate = createServerValidate({
validatorAdapter: yupValidator(),
onServerValidate: yup.object({
name: yup.string().test('Testing 123', 'Testing 123', async (val) => {
await sleep(1)
return typeof val === 'string' ? val.length > 3 : false
}),
}),
})

const formData1 = new FormData()
formData1.append('name', 'a')
expect(
await serverValidate(formData1).catch((e) => e.formState.errors),
).toEqual(['Testing 123'])

const formData2 = new FormData()
formData2.append('name', 'asdf')
expect(await serverValidate(formData2)).toBeUndefined()
})

it('should run async fn with yup validation option enabled', async () => {
const serverValidate = createServerValidate({
validatorAdapter: yupValidator(),
onServerValidate: async ({ value }: { value: { name: string } }) => {
await sleep(1)
return value.name === 'a' ? 'Test' : undefined
},
})

const formData1 = new FormData()
formData1.append('name', 'a')
expect(
await serverValidate(formData1).catch((e) => e.formState.errors),
).toEqual(['Test'])

const formData2 = new FormData()
formData2.append('name', 'asdf')
expect(await serverValidate(formData2)).toBeUndefined()
})

it('should transform errors to display all error message', async () => {
const serverValidate = createServerValidate({
validatorAdapter: yupValidator(),
onServerValidate: yup.object({
name: yup
.string()
.min(3, 'You must have a length of at least 3')
.uuid('UUID'),
}),
})

const formData1 = new FormData()
formData1.append('name', 'aa')
expect(
await serverValidate(formData1).catch((e) => e.formState.errors),
).toEqual(['You must have a length of at least 3'])

const formData2 = new FormData()
formData2.append('name', 'aaa')
expect(
await serverValidate(formData2).catch((e) => e.formState.errors),
).toEqual(['UUID'])
})
})
3 changes: 2 additions & 1 deletion packages/zod-form-adapter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
"@tanstack/form-core": "workspace:*"
},
"devDependencies": {
"zod": "^3.23.8"
"zod": "^3.23.8",
"@tanstack/react-form": "workspace:*"
},
"peerDependencies": {
"zod": "^3.x"
Expand Down
Loading

0 comments on commit 4e7f01d

Please sign in to comment.