UNPKG

@tanstack/start-client-core

Version:

Modern and scalable routing for React applications

867 lines (735 loc) 23.6 kB
import { describe, expectTypeOf, test } from 'vitest' import { createMiddleware } from '../createMiddleware' import { createServerFn } from '../createServerFn' import { TSS_SERVER_FUNCTION } from '../constants' import type { ServerFnMeta } from '../constants' import type { Constrain, Register, TsrSerializable, ValidateSerializableInput, Validator, } from '@tanstack/router-core' import type { ConstrainValidator, CustomFetch, ServerFnReturnType, } from '../createServerFn' test('createServerFn without middleware', () => { expectTypeOf(createServerFn()).toHaveProperty('handler') expectTypeOf(createServerFn()).toHaveProperty('middleware') expectTypeOf(createServerFn()).toHaveProperty('inputValidator') createServerFn({ method: 'GET' }).handler((options) => { expectTypeOf(options).toEqualTypeOf<{ context: undefined data: undefined method: 'GET' serverFnMeta: ServerFnMeta }>() }) }) test('createServerFn with validator function', () => { const fnAfterValidator = createServerFn({ method: 'GET', }).inputValidator((input: { input: string }) => ({ a: input.input, })) expectTypeOf(fnAfterValidator).toHaveProperty('handler') expectTypeOf(fnAfterValidator).toHaveProperty('middleware') expectTypeOf(fnAfterValidator).not.toHaveProperty('inputValidator') const fn = fnAfterValidator.handler((options) => { expectTypeOf(options).toEqualTypeOf<{ context: undefined data: { a: string } method: 'GET' serverFnMeta: ServerFnMeta }>() }) expectTypeOf(fn).parameter(0).toEqualTypeOf<{ data: { input: string } headers?: HeadersInit signal?: AbortSignal fetch?: CustomFetch }>() expectTypeOf<ReturnType<typeof fn>>().resolves.toEqualTypeOf<void>() }) test('createServerFn with async validator function', () => { const fnAfterValidator = createServerFn({ method: 'GET', }).inputValidator((input: string) => Promise.resolve(input)) expectTypeOf(fnAfterValidator).toHaveProperty('handler') expectTypeOf(fnAfterValidator).toHaveProperty('middleware') expectTypeOf(fnAfterValidator).not.toHaveProperty('inputValidator') const fn = fnAfterValidator.handler((options) => { expectTypeOf(options).toEqualTypeOf<{ context: undefined data: string method: 'GET' serverFnMeta: ServerFnMeta }>() }) expectTypeOf(fn).parameter(0).toEqualTypeOf<{ data: string headers?: HeadersInit signal?: AbortSignal fetch?: CustomFetch }>() expectTypeOf<ReturnType<typeof fn>>().resolves.toEqualTypeOf<void>() }) test('createServerFn with validator with parse method', () => { const fnAfterValidator = createServerFn({ method: 'GET', }).inputValidator({ parse: (input: string) => input, }) expectTypeOf(fnAfterValidator).toHaveProperty('handler') expectTypeOf(fnAfterValidator).toHaveProperty('middleware') expectTypeOf(fnAfterValidator).not.toHaveProperty('inputValidator') const fn = fnAfterValidator.handler((options) => { expectTypeOf(options).toEqualTypeOf<{ context: undefined data: string method: 'GET' serverFnMeta: ServerFnMeta }>() }) expectTypeOf(fn).parameter(0).toEqualTypeOf<{ data: string headers?: HeadersInit signal?: AbortSignal fetch?: CustomFetch }>() expectTypeOf<ReturnType<typeof fn>>().resolves.toEqualTypeOf<void>() }) test('createServerFn with async validator with parse method', () => { const fnAfterValidator = createServerFn({ method: 'GET', }).inputValidator({ parse: (input: string) => Promise.resolve(input), }) expectTypeOf(fnAfterValidator).toHaveProperty('handler') expectTypeOf(fnAfterValidator).toHaveProperty('middleware') expectTypeOf(fnAfterValidator).not.toHaveProperty('inputValidator') const fn = fnAfterValidator.handler((options) => { expectTypeOf(options).toEqualTypeOf<{ context: undefined data: string method: 'GET' serverFnMeta: ServerFnMeta }>() }) expectTypeOf(fn).parameter(0).toEqualTypeOf<{ data: string headers?: HeadersInit signal?: AbortSignal fetch?: CustomFetch }>() expectTypeOf<ReturnType<typeof fn>>().resolves.toEqualTypeOf<void>() }) test('createServerFn with standard validator', () => { interface SyncValidator { readonly '~standard': { types?: { input: string output: string } validate: (input: unknown) => { value: string } } } const validator: SyncValidator = { ['~standard']: { validate: (input: unknown) => ({ value: input as string, }), }, } const fnAfterValidator = createServerFn({ method: 'GET', }).inputValidator(validator) expectTypeOf(fnAfterValidator).toHaveProperty('handler') expectTypeOf(fnAfterValidator).toHaveProperty('middleware') expectTypeOf(fnAfterValidator).not.toHaveProperty('inputValidator') const fn = fnAfterValidator.handler((options) => { expectTypeOf(options).toEqualTypeOf<{ context: undefined data: string method: 'GET' serverFnMeta: ServerFnMeta }>() }) expectTypeOf(fn).parameter(0).toEqualTypeOf<{ data: string headers?: HeadersInit signal?: AbortSignal fetch?: CustomFetch }>() expectTypeOf<ReturnType<typeof fn>>().resolves.toEqualTypeOf<void>() }) test('createServerFn with async standard validator', () => { interface AsyncValidator { readonly '~standard': { types?: { input: string output: string } validate: (input: unknown) => Promise<{ value: string }> } } const validator: AsyncValidator = { ['~standard']: { validate: (input: unknown) => Promise.resolve({ value: input as string, }), }, } const fnAfterValidator = createServerFn({ method: 'GET', }).inputValidator(validator) expectTypeOf(fnAfterValidator).toHaveProperty('handler') expectTypeOf(fnAfterValidator).toHaveProperty('middleware') expectTypeOf(fnAfterValidator).not.toHaveProperty('inputValidator') const fn = fnAfterValidator.handler((options) => { expectTypeOf(options).toEqualTypeOf<{ context: undefined data: string method: 'GET' serverFnMeta: ServerFnMeta }>() }) expectTypeOf(fn).parameter(0).toEqualTypeOf<{ data: string headers?: HeadersInit signal?: AbortSignal fetch?: CustomFetch }>() expectTypeOf<ReturnType<typeof fn>>().resolves.toEqualTypeOf<void>() }) test('createServerFn with middleware and context', () => { const middleware1 = createMiddleware({ type: 'function' }).server( ({ next }) => { return next({ context: { a: 'a' } as const }) }, ) const middleware2 = createMiddleware({ type: 'function' }).server( ({ next }) => { return next({ context: { b: 'b' } as const }) }, ) const middleware3 = createMiddleware({ type: 'function' }) .middleware([middleware1, middleware2]) .client(({ next }) => { return next({ context: { c: 'c' } as const }) }) const middleware4 = createMiddleware({ type: 'function' }) .middleware([middleware3]) .client(({ context, next }) => { return next({ sendContext: context }) }) .server(({ context, next }) => { expectTypeOf(context).toEqualTypeOf<{ readonly a: 'a' readonly b: 'b' readonly c: 'c' }>() return next({ context: { d: 'd' } as const }) }) const fnWithMiddleware = createServerFn({ method: 'GET' }).middleware([ middleware4, ]) expectTypeOf(fnWithMiddleware).toHaveProperty('handler') expectTypeOf(fnWithMiddleware).toHaveProperty('inputValidator') fnWithMiddleware.handler((options) => { expectTypeOf(options).toEqualTypeOf<{ context: { readonly a: 'a' readonly b: 'b' readonly c: 'c' readonly d: 'd' } data: undefined method: 'GET' serverFnMeta: ServerFnMeta }>() }) }) describe('createServerFn with middleware and validator', () => { const middleware1 = createMiddleware({ type: 'function' }).inputValidator( (input: { readonly inputA: 'inputA' }) => ({ outputA: 'outputA', }) as const, ) const middleware2 = createMiddleware({ type: 'function' }).inputValidator( (input: { readonly inputB: 'inputB' }) => ({ outputB: 'outputB', }) as const, ) const middleware3 = createMiddleware({ type: 'function' }).middleware([ middleware1, middleware2, ]) test(`response`, () => { const fn = createServerFn({ method: 'GET' }) .middleware([middleware3]) .inputValidator( (input: { readonly inputC: 'inputC' }) => ({ outputC: 'outputC', }) as const, ) .handler((options) => { expectTypeOf(options).toEqualTypeOf<{ context: undefined data: { readonly outputA: 'outputA' readonly outputB: 'outputB' readonly outputC: 'outputC' } method: 'GET' serverFnMeta: ServerFnMeta }>() return 'some-data' as const }) expectTypeOf(fn).parameter(0).toEqualTypeOf<{ data: { readonly inputA: 'inputA' readonly inputB: 'inputB' readonly inputC: 'inputC' } headers?: HeadersInit signal?: AbortSignal fetch?: CustomFetch }>() expectTypeOf(fn).returns.resolves.toEqualTypeOf<'some-data'>() expectTypeOf(() => fn({ data: { inputA: 'inputA', inputB: 'inputB', inputC: 'inputC' }, }), ).returns.resolves.toEqualTypeOf<'some-data'>() }) }) test('createServerFn overrides properties', () => { const middleware1 = createMiddleware({ type: 'function' }) .inputValidator( () => ({ input: 'a' as 'a' | 'b' | 'c', }) as const, ) .client(({ context, next }) => { expectTypeOf(context).toEqualTypeOf<undefined>() const newContext = { context: 'a' } as const return next({ sendContext: newContext, context: newContext }) }) .server(({ data, context, next }) => { expectTypeOf(data).toEqualTypeOf<{ readonly input: 'a' | 'b' | 'c' }>() expectTypeOf(context).toEqualTypeOf<{ readonly context: 'a' }>() const newContext = { context: 'b' } as const return next({ sendContext: newContext, context: newContext }) }) const middleware2 = createMiddleware({ type: 'function' }) .middleware([middleware1]) .inputValidator( () => ({ input: 'b' as 'b' | 'c', }) as const, ) .client(({ context, next }) => { expectTypeOf(context).toEqualTypeOf<{ readonly context: 'a' }>() const newContext = { context: 'aa' } as const return next({ sendContext: newContext, context: newContext }) }) .server(({ context, next }) => { expectTypeOf(context).toEqualTypeOf<{ readonly context: 'aa' }>() const newContext = { context: 'bb' } as const return next({ sendContext: newContext, context: newContext }) }) createServerFn() .middleware([middleware2]) .inputValidator( () => ({ input: 'c', }) as const, ) .handler(({ data, context }) => { expectTypeOf(data).toEqualTypeOf<{ readonly input: 'c' }>() expectTypeOf(context).toEqualTypeOf<{ readonly context: 'bb' }>() }) }) test('createServerFn where validator is a primitive', () => { createServerFn({ method: 'GET' }) .inputValidator(() => 'c' as const) .handler((options) => { expectTypeOf(options).toEqualTypeOf<{ context: undefined data: 'c' method: 'GET' serverFnMeta: ServerFnMeta }>() }) }) test('createServerFn where validator is optional if object is optional', () => { const fn = createServerFn({ method: 'GET' }) .inputValidator((input: 'c' | undefined) => input) .handler((options) => { expectTypeOf(options).toEqualTypeOf<{ context: undefined data: 'c' | undefined method: 'GET' serverFnMeta: ServerFnMeta }>() }) expectTypeOf(fn).parameter(0).toEqualTypeOf< | { data?: 'c' | undefined headers?: HeadersInit signal?: AbortSignal fetch?: CustomFetch } | undefined >() expectTypeOf<ReturnType<typeof fn>>().resolves.toEqualTypeOf<void>() }) test('createServerFn where data is optional if there is no validator', () => { const fn = createServerFn({ method: 'GET' }).handler((options) => { expectTypeOf(options).toEqualTypeOf<{ context: undefined data: undefined method: 'GET' serverFnMeta: ServerFnMeta }>() }) expectTypeOf(fn).parameter(0).toEqualTypeOf< | { data?: undefined headers?: HeadersInit signal?: AbortSignal fetch?: CustomFetch } | undefined >() expectTypeOf<ReturnType<typeof fn>>().resolves.toEqualTypeOf<void>() }) test('createServerFn returns Date', () => { const fn = createServerFn().handler(() => ({ dates: [new Date(), new Date()] as const, })) expectTypeOf<ReturnType<typeof fn>>().toMatchTypeOf<Promise<unknown>>() expectTypeOf<Awaited<ReturnType<typeof fn>>>().toMatchTypeOf< ValidateSerializableInput<Register, { dates: readonly [Date, Date] }> >() }) test('createServerFn returns undefined', () => { const fn = createServerFn().handler(() => ({ nothing: undefined, })) expectTypeOf(fn()).toEqualTypeOf<Promise<{ nothing: undefined }>>() }) test('createServerFn cannot return function', () => { expectTypeOf(createServerFn().handler<{ func: () => 'func' }>) .parameter(0) .returns.toEqualTypeOf<{ func: 'Function is not serializable' }>() }) test('createServerFn cannot validate function', () => { const validator = createServerFn().inputValidator< (input: { func: () => 'string' }) => { output: 'string' } > expectTypeOf(validator) .parameter(0) .toEqualTypeOf< Constrain< (input: { func: () => 'string' }) => { output: 'string' }, Validator<{ func: 'Function is not serializable' }, any> > >() }) test('createServerFn can validate Date', () => { const validator = createServerFn().inputValidator< (input: Date) => { output: 'string' } > expectTypeOf(validator) .parameter(0) .toEqualTypeOf< ConstrainValidator<Register, 'GET', (input: Date) => { output: 'string' }> >() }) test('createServerFn can validate FormData', () => { const validator = createServerFn({ method: 'POST' }).inputValidator< (input: FormData) => { output: 'string' } > expectTypeOf(validator).parameter(0).parameter(0).toEqualTypeOf<FormData>() }) test('createServerFn cannot validate FormData for GET', () => { const validator = createServerFn({ method: 'GET' }).inputValidator< (input: FormData) => { output: 'string' } > expectTypeOf(validator) .parameter(0) .parameter(0) .not.toEqualTypeOf<FormData>() }) describe('response', () => { test(`client receives Response when Response is returned`, () => { const fn = createServerFn().handler(() => { return new Response('Hello World') }) expectTypeOf(fn()).toEqualTypeOf<Promise<Response>>() }) test(`client receives union when handler may return Response or string`, () => { const fn = createServerFn().handler(() => { const result: Response | 'Hello World' = Math.random() > 0.5 ? new Response('Hello World') : 'Hello World' return result }) expectTypeOf(fn()).toEqualTypeOf<Promise<Response | 'Hello World'>>() }) }) test('ServerFnReturnType distributes Response union', () => { expectTypeOf< ServerFnReturnType<Register, Response | 'Hello World'> >().toEqualTypeOf<Response | 'Hello World'>() }) test('createServerFn can be used as a mutation function', () => { const serverFn = createServerFn() .inputValidator((data: number) => data) .handler(() => 'foo') type MutationFunction<TData = unknown, TVariables = unknown> = ( variables: TVariables, ) => Promise<TData> // simplifeid "clone" of @tansctack/react-query's useMutation const useMutation = <TData, TVariables>( fn: MutationFunction<TData, TVariables>, ) => {} useMutation(serverFn) }) test('createServerFn validator infers unknown for default input type', () => { const fn = createServerFn() .inputValidator((input) => { expectTypeOf(input).toEqualTypeOf<unknown>() if (typeof input === 'number') return 'success' as const return 'failed' as const }) .handler(({ data }) => { expectTypeOf(data).toEqualTypeOf<'success' | 'failed'>() return data }) expectTypeOf(fn).parameter(0).toEqualTypeOf< | { data?: unknown | undefined headers?: HeadersInit signal?: AbortSignal fetch?: CustomFetch } | undefined >() expectTypeOf(fn()).toEqualTypeOf<Promise<'failed' | 'success'>>() }) test('incrementally building createServerFn with multiple middleware calls', () => { const middleware1 = createMiddleware({ type: 'function' }).server( ({ next }) => { return next({ context: { a: 'a' } as const }) }, ) const middleware2 = createMiddleware({ type: 'function' }).server( ({ next }) => { return next({ context: { b: 'b' } as const }) }, ) const middleware3 = createMiddleware({ type: 'function' }).server( ({ next }) => { return next({ context: { c: 'c' } as const }) }, ) const builderWithMw1 = createServerFn({ method: 'GET' }).middleware([ middleware1, ]) expectTypeOf(builderWithMw1).toHaveProperty('handler') expectTypeOf(builderWithMw1).toHaveProperty('inputValidator') expectTypeOf(builderWithMw1).toHaveProperty('middleware') builderWithMw1.handler((options) => { expectTypeOf(options).toEqualTypeOf<{ context: { readonly a: 'a' } data: undefined method: 'GET' serverFnMeta: ServerFnMeta }>() }) // overrides method const builderWithMw2 = builderWithMw1({ method: 'POST' }).middleware([ middleware2, ]) expectTypeOf(builderWithMw2).toHaveProperty('handler') expectTypeOf(builderWithMw2).toHaveProperty('inputValidator') expectTypeOf(builderWithMw2).toHaveProperty('middleware') builderWithMw2.handler((options) => { expectTypeOf(options).toEqualTypeOf<{ context: { readonly a: 'a' readonly b: 'b' } data: undefined method: 'POST' serverFnMeta: ServerFnMeta }>() }) // overrides method again const builderWithMw3 = builderWithMw2({ method: 'GET' }).middleware([ middleware3, ]) expectTypeOf(builderWithMw3).toHaveProperty('handler') expectTypeOf(builderWithMw3).toHaveProperty('inputValidator') expectTypeOf(builderWithMw3).toHaveProperty('middleware') builderWithMw3.handler((options) => { expectTypeOf(options).toEqualTypeOf<{ context: { readonly a: 'a' readonly b: 'b' readonly c: 'c' } data: undefined method: 'GET' serverFnMeta: ServerFnMeta }>() }) }) test('compose middlewares and server function factories', () => { const middleware1 = createMiddleware({ type: 'function' }).server( ({ next }) => { return next({ context: { a: 'a' } as const }) }, ) const middleware2 = createMiddleware({ type: 'function' }).server( ({ next }) => { return next({ context: { b: 'b' } as const }) }, ) const builderWithMw1 = createServerFn().middleware([middleware1]) const composedBuilder = createServerFn({ method: 'GET' }).middleware([ middleware2, builderWithMw1, ]) composedBuilder.handler((options) => { expectTypeOf(options).toEqualTypeOf<{ context: { readonly a: 'a' readonly b: 'b' } data: undefined method: 'GET' serverFnMeta: ServerFnMeta }>() }) }) test('createServerFn with request middleware', () => { const reqMw = createMiddleware().server(({ next }) => { return next() }) const fn = createServerFn() .middleware([reqMw]) .handler(() => ({})) expectTypeOf(fn()).toEqualTypeOf<Promise<{}>>() }) test('createServerFn with request middleware and function middleware', () => { const reqMw = createMiddleware().server(({ next }) => { return next() }) const funMw = createMiddleware({ type: 'function' }) .inputValidator((x: string) => x) .server(({ next }) => { return next({ context: { a: 'a' } as const }) }) const fn = createServerFn() .middleware([reqMw, funMw]) .handler(() => ({})) expectTypeOf(fn({ data: 'a' })).toEqualTypeOf<Promise<{}>>() }) test('createServerFn with inputValidator and request middleware', () => { const loggingMiddleware = createMiddleware().server(async ({ next }) => { console.log('Logging middleware executed on the server') const result = await next() return result }) const fn = createServerFn() .middleware([loggingMiddleware]) .inputValidator(({ userName }: { userName: string }) => { return { userName } }) .handler(async ({ data }) => { return data.userName }) expectTypeOf(fn({ data: { userName: 'test' } })).toEqualTypeOf< Promise<string> >() }) test('createServerFn has TSS_SERVER_FUNCTION symbol set', () => { const fn = createServerFn().handler(() => ({})) expectTypeOf(fn).toHaveProperty(TSS_SERVER_FUNCTION) expectTypeOf(fn[TSS_SERVER_FUNCTION]).toEqualTypeOf<true>() }) test('createServerFn fetcher itself is serializable', () => { const fn1 = createServerFn().handler(() => ({})) const fn2 = createServerFn().handler(() => fn1) }) test('createServerFn returns async Response', () => { const serverFn = createServerFn().handler(async () => { return new Response(new Blob([JSON.stringify({ a: 1 })]), { status: 200, headers: { 'Content-Type': 'application/json', }, }) }) expectTypeOf(serverFn()).toEqualTypeOf<Promise<Response>>() }) test('createServerFn returns sync Response', () => { const serverFn = createServerFn().handler(() => { return new Response(new Blob([JSON.stringify({ a: 1 })]), { status: 200, headers: { 'Content-Type': 'application/json', }, }) }) expectTypeOf(serverFn()).toEqualTypeOf<Promise<Response>>() }) test('createServerFn returns async array', () => { const result: Array<{ a: number }> = [{ a: 1 }] const serverFn = createServerFn({ method: 'GET' }).handler(async () => { return result }) expectTypeOf(serverFn()).toEqualTypeOf<Promise<Array<{ a: number }>>>() }) test('createServerFn returns sync array', () => { const result: Array<{ a: number }> = [{ a: 1 }] const serverFn = createServerFn({ method: 'GET' }).handler(() => { return result }) expectTypeOf(serverFn()).toEqualTypeOf<Promise<Array<{ a: number }>>>() }) test('createServerFn respects TsrSerializable', () => { type MyCustomType = { f: () => void; value: string } type MyCustomTypeSerializable = MyCustomType & TsrSerializable const fn1 = createServerFn().handler(() => { const custom: MyCustomType = { f: () => {}, value: 'test' } return { nested: { custom: custom as MyCustomTypeSerializable } } }) expectTypeOf(fn1()).toEqualTypeOf< Promise<{ nested: { custom: MyCustomTypeSerializable } }> >() })