Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions e2e/react-start/server-functions/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { Route as HeadersRouteImport } from './routes/headers'
import { Route as EnvOnlyRouteImport } from './routes/env-only'
import { Route as DeadCodePreserveRouteImport } from './routes/dead-code-preserve'
import { Route as ConsistentRouteImport } from './routes/consistent'
import { Route as AsyncValidationRouteImport } from './routes/async-validation'
import { Route as AbortSignalRouteImport } from './routes/abort-signal'
import { Route as IndexRouteImport } from './routes/index'
import { Route as PrimitivesIndexRouteImport } from './routes/primitives/index'
Expand Down Expand Up @@ -88,6 +89,11 @@ const ConsistentRoute = ConsistentRouteImport.update({
path: '/consistent',
getParentRoute: () => rootRouteImport,
} as any)
const AsyncValidationRoute = AsyncValidationRouteImport.update({
id: '/async-validation',
path: '/async-validation',
getParentRoute: () => rootRouteImport,
} as any)
const AbortSignalRoute = AbortSignalRouteImport.update({
id: '/abort-signal',
path: '/abort-signal',
Expand Down Expand Up @@ -155,6 +161,7 @@ const FormdataRedirectTargetNameRoute =
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/abort-signal': typeof AbortSignalRoute
'/async-validation': typeof AsyncValidationRoute
'/consistent': typeof ConsistentRoute
'/dead-code-preserve': typeof DeadCodePreserveRoute
'/env-only': typeof EnvOnlyRoute
Expand All @@ -180,6 +187,7 @@ export interface FileRoutesByFullPath {
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/abort-signal': typeof AbortSignalRoute
'/async-validation': typeof AsyncValidationRoute
'/consistent': typeof ConsistentRoute
'/dead-code-preserve': typeof DeadCodePreserveRoute
'/env-only': typeof EnvOnlyRoute
Expand All @@ -206,6 +214,7 @@ export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/abort-signal': typeof AbortSignalRoute
'/async-validation': typeof AsyncValidationRoute
'/consistent': typeof ConsistentRoute
'/dead-code-preserve': typeof DeadCodePreserveRoute
'/env-only': typeof EnvOnlyRoute
Expand Down Expand Up @@ -233,6 +242,7 @@ export interface FileRouteTypes {
fullPaths:
| '/'
| '/abort-signal'
| '/async-validation'
| '/consistent'
| '/dead-code-preserve'
| '/env-only'
Expand All @@ -258,6 +268,7 @@ export interface FileRouteTypes {
to:
| '/'
| '/abort-signal'
| '/async-validation'
| '/consistent'
| '/dead-code-preserve'
| '/env-only'
Expand All @@ -283,6 +294,7 @@ export interface FileRouteTypes {
| '__root__'
| '/'
| '/abort-signal'
| '/async-validation'
| '/consistent'
| '/dead-code-preserve'
| '/env-only'
Expand All @@ -309,6 +321,7 @@ export interface FileRouteTypes {
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AbortSignalRoute: typeof AbortSignalRoute
AsyncValidationRoute: typeof AsyncValidationRoute
ConsistentRoute: typeof ConsistentRoute
DeadCodePreserveRoute: typeof DeadCodePreserveRoute
EnvOnlyRoute: typeof EnvOnlyRoute
Expand Down Expand Up @@ -411,6 +424,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ConsistentRouteImport
parentRoute: typeof rootRouteImport
}
'/async-validation': {
id: '/async-validation'
path: '/async-validation'
fullPath: '/async-validation'
preLoaderRoute: typeof AsyncValidationRouteImport
parentRoute: typeof rootRouteImport
}
'/abort-signal': {
id: '/abort-signal'
path: '/abort-signal'
Expand Down Expand Up @@ -501,6 +521,7 @@ declare module '@tanstack/react-router' {
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AbortSignalRoute: AbortSignalRoute,
AsyncValidationRoute: AsyncValidationRoute,
ConsistentRoute: ConsistentRoute,
DeadCodePreserveRoute: DeadCodePreserveRoute,
EnvOnlyRoute: EnvOnlyRoute,
Expand Down
64 changes: 64 additions & 0 deletions e2e/react-start/server-functions/src/routes/async-validation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
import React from 'react'
import { z } from 'zod'

export const Route = createFileRoute('/async-validation')({
component: RouteComponent,
})

const asyncValidationSchema = z
.string()
.refine((data) => Promise.resolve(data !== 'invalid'))

const asyncValidationServerFn = createServerFn()
.inputValidator(asyncValidationSchema)
.handler(({ data }) => data)

function RouteComponent() {
const [errorMessage, setErrorMessage] = React.useState<string | undefined>(
undefined,
)
const [result, setResult] = React.useState<string | undefined>(undefined)

const callServerFn = async (value: string) => {
setErrorMessage(undefined)
setResult(undefined)

try {
const serverFnResult = await asyncValidationServerFn({ data: value })
setResult(serverFnResult)
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : 'unknown')
}
}

return (
<div>
<button
data-testid="run-with-valid-btn"
onClick={() => {
callServerFn('valid')
}}
>
call server function with valid value
</button>
<br />
<button
data-testid="run-with-invalid-btn"
onClick={() => {
callServerFn('invalid')
}}
>
call server function with invalid value
</button>
<div className="p-2">
result: <p data-testid="result">{result ?? '$undefined'}</p>
</div>
<div className="p-2">
message:{' '}
<p data-testid="errorMessage">{errorMessage ?? '$undefined'}</p>
</div>
</div>
)
}
7 changes: 6 additions & 1 deletion e2e/react-start/server-functions/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,17 @@ function Home() {
</li>
<li>
<Link to="/dead-code-preserve">
dead code elimation only affects code after transformation
dead code elimination only affects code after transformation
</Link>
</li>
<li>
<Link to="/abort-signal">aborting a server function call</Link>
</li>
<li>
<Link to="/async-validation">
server function with async validation
</Link>
</li>
<li>
<Link to="/raw-response">server function returns raw response</Link>
</li>
Expand Down
48 changes: 48 additions & 0 deletions e2e/react-start/server-functions/tests/server-functions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,54 @@ test.describe('aborting a server function call', () => {
})
})

test.describe('server functions with async validation', () => {
test.use({
whitelistErrors: [
/Failed to load resource: the server responded with a status of 500/,
],
})

test('with valid input', async ({ page }) => {
await page.goto('/async-validation')

await page.waitForLoadState('networkidle')

await page.getByTestId('run-with-valid-btn').click()
await page.waitForLoadState('networkidle')
await page.waitForSelector('[data-testid="result"]:has-text("valid")')
await page.waitForSelector(
'[data-testid="errorMessage"]:has-text("$undefined")',
)

const result = (await page.getByTestId('result').textContent()) || ''
expect(result).toBe('valid')

const errorMessage =
(await page.getByTestId('errorMessage').textContent()) || ''
expect(errorMessage).toBe('$undefined')
})

test('with invalid input', async ({ page }) => {
await page.goto('/async-validation')

await page.waitForLoadState('networkidle')

await page.getByTestId('run-with-invalid-btn').click()
await page.waitForLoadState('networkidle')
await page.waitForSelector('[data-testid="result"]:has-text("$undefined")')
await page.waitForSelector(
'[data-testid="errorMessage"]:has-text("invalid")',
)

const result = (await page.getByTestId('result').textContent()) || ''
expect(result).toBe('$undefined')

const errorMessage =
(await page.getByTestId('errorMessage').textContent()) || ''
expect(errorMessage).toContain('Invalid input')
})
})

test('raw response', async ({ page }) => {
await page.goto('/raw-response')

Expand Down
2 changes: 1 addition & 1 deletion packages/start-client-core/src/createMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ export type IntersectAllValidatorOutputs<TMiddlewares, TInputValidator> =
? IntersectAllMiddleware<TMiddlewares, 'allOutput'>
: IntersectAssign<
IntersectAllMiddleware<TMiddlewares, 'allOutput'>,
ResolveValidatorOutput<TInputValidator>
Awaited<ResolveValidatorOutput<TInputValidator>>
>

/**
Expand Down
9 changes: 3 additions & 6 deletions packages/start-client-core/src/createServerFn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -663,17 +663,14 @@ export const applyMiddleware = async (
} as any)
}

export function execValidator(
export async function execValidator(
validator: AnyValidator,
input: unknown,
): unknown {
): Promise<unknown> {
if (validator == null) return {}

if ('~standard' in validator) {
const result = validator['~standard'].validate(input)

if (result instanceof Promise)
throw new Error('Async validation not supported')
const result = await validator['~standard'].validate(input)

if (result.issues)
throw new Error(JSON.stringify(result.issues, undefined, 2))
Expand Down
Loading
Loading