diff --git a/.changeset/strict-server-functions.md b/.changeset/strict-server-functions.md new file mode 100644 index 0000000000..f33e5c64a8 --- /dev/null +++ b/.changeset/strict-server-functions.md @@ -0,0 +1,5 @@ +--- +'@tanstack/start-client-core': minor +--- + +Add a `strict` option to `createServerFn` for type-level server function serialization checks. Use `strict: false` to opt out for input and output values, or `strict: { output: false }` to opt out for return values only. diff --git a/packages/start-client-core/src/createServerFn.ts b/packages/start-client-core/src/createServerFn.ts index c46fb769f6..37d4ead122 100644 --- a/packages/start-client-core/src/createServerFn.ts +++ b/packages/start-client-core/src/createServerFn.ts @@ -32,23 +32,47 @@ import type { type TODO = any +export type ServerFnStrict = boolean | { input?: boolean; output?: boolean } + +export interface ServerFnOptions< + TMethod extends Method = Method, + TStrict extends ServerFnStrict = true, +> { + method?: TMethod + strict?: TStrict +} + +export type ServerFnStrictInput = + TStrict extends false + ? false + : TStrict extends { input: infer TInput extends boolean } + ? TInput + : true + +export type ServerFnStrictOutput = + TStrict extends false + ? false + : TStrict extends { output: infer TOutput extends boolean } + ? TOutput + : true + export type CreateServerFn = < TMethod extends Method, + TStrict extends ServerFnStrict = true, TResponse = unknown, TMiddlewares = undefined, TInputValidator = undefined, >( - options?: { - method?: TMethod - }, + options?: ServerFnOptions, __opts?: ServerFnBaseOptions< TRegister, TMethod, TResponse, TMiddlewares, - TInputValidator + TInputValidator, + TStrict >, -) => ServerFnBuilder +) => ServerFnBuilder export const createServerFn: CreateServerFn = (options, __opts) => { const resolvedOptions = (__opts || options || {}) as ServerFnBaseOptions< @@ -56,6 +80,7 @@ export const createServerFn: CreateServerFn = (options, __opts) => { any, any, any, + any, any > @@ -63,7 +88,7 @@ export const createServerFn: CreateServerFn = (options, __opts) => { resolvedOptions.method = 'GET' as Method } - const res: ServerFnBuilder = { + const res: ServerFnBuilder = { options: resolvedOptions, middleware: (middleware) => { // multiple calls to `middleware()` merge the middlewares with the previously supplied ones @@ -183,8 +208,8 @@ export const createServerFn: CreateServerFn = (options, __opts) => { }, ) as any }, - } as ServerFnBuilder - const fun = (options?: { method?: Method }) => { + } as ServerFnBuilder + const fun = (options?: ServerFnOptions) => { const newOptions = { ...resolvedOptions, ...options, @@ -414,12 +439,18 @@ export type RscStream = { export type Method = 'GET' | 'POST' -export type ServerFnReturnType = - TResponse extends PromiseLike - ? Promise> - : TResponse extends Response - ? TResponse - : ValidateSerializableInput +export type ServerFnReturnType< + TRegister, + TResponse, + TStrict extends ServerFnStrict = true, +> = + ServerFnStrictOutput extends false + ? TResponse + : TResponse extends PromiseLike + ? Promise> + : TResponse extends Response + ? TResponse + : ValidateSerializableInput export type ServerFn< TRegister, @@ -427,9 +458,10 @@ export type ServerFn< TMiddlewares, TInputValidator, TResponse, + TStrict extends ServerFnStrict = true, > = ( ctx: ServerFnCtx, -) => ServerFnReturnType +) => ServerFnReturnType export interface ServerFnCtx< TRegister, @@ -457,20 +489,28 @@ export type ServerFnBaseOptions< TResponse = unknown, TMiddlewares = unknown, TInputValidator = unknown, + TStrict extends ServerFnStrict = true, > = { method: TMethod + strict?: TStrict middleware?: Constrain< TMiddlewares, ReadonlyArray > - inputValidator?: ConstrainValidator + inputValidator?: ConstrainValidator< + TRegister, + TMethod, + TInputValidator, + TStrict + > extractedFn?: CompiledFetcherFn serverFn?: ServerFn< TRegister, TMethod, TMiddlewares, TInputValidator, - TResponse + TResponse, + TStrict > } @@ -478,27 +518,33 @@ export type ValidateValidatorInput< TRegister, TMethod extends Method, TInputValidator, -> = TMethod extends 'POST' - ? ResolveValidatorInput extends FormData + TStrict extends ServerFnStrict = true, +> = + ServerFnStrictInput extends false ? ResolveValidatorInput - : ValidateSerializable< - ResolveValidatorInput, - RegisteredSerializableInput - > - : ValidateSerializable< - ResolveValidatorInput, - RegisteredSerializableInput - > + : TMethod extends 'POST' + ? ResolveValidatorInput extends FormData + ? ResolveValidatorInput + : ValidateSerializable< + ResolveValidatorInput, + RegisteredSerializableInput + > + : ValidateSerializable< + ResolveValidatorInput, + RegisteredSerializableInput + > export type ValidateValidator< TRegister, TMethod extends Method, TInputValidator, + TStrict extends ServerFnStrict = true, > = ValidateValidatorInput< TRegister, TMethod, - TInputValidator + TInputValidator, + TStrict > extends infer TInput ? Validator : never @@ -507,17 +553,19 @@ export type ConstrainValidator< TRegister, TMethod extends Method, TInputValidator, + TStrict extends ServerFnStrict = true, > = | (unknown extends TInputValidator ? TInputValidator : ResolveValidatorInput extends ValidateValidator< TRegister, TMethod, - TInputValidator + TInputValidator, + TStrict > ? TInputValidator : never) - | ValidateValidator + | ValidateValidator export type AppendMiddlewares = TMiddlewares extends ReadonlyArray @@ -531,6 +579,7 @@ export interface ServerFnMiddleware< TMethod extends Method, TMiddlewares, TInputValidator, + TStrict extends ServerFnStrict, > { middleware: ( middlewares: Constrain< @@ -541,7 +590,8 @@ export interface ServerFnMiddleware< TRegister, TMethod, AppendMiddlewares, - TInputValidator + TInputValidator, + TStrict > } @@ -550,6 +600,7 @@ export interface ServerFnAfterMiddleware< TMethod extends Method, TMiddlewares, TInputValidator, + TStrict extends ServerFnStrict, > extends ServerFnWithTypes< @@ -557,33 +608,59 @@ export interface ServerFnAfterMiddleware< TMethod, TMiddlewares, TInputValidator, - undefined + undefined, + TStrict >, - ServerFnMiddleware, - ServerFnValidator, - ServerFnHandler { - (options?: { - method?: TNewMethod - }): ServerFnAfterMiddleware< + ServerFnMiddleware, + ServerFnValidator, + ServerFnHandler< + TRegister, + TMethod, + TMiddlewares, + TInputValidator, + TStrict + > { + < + TNewMethod extends Method = TMethod, + TNewStrict extends ServerFnStrict = TStrict, + >( + options?: ServerFnOptions, + ): ServerFnAfterMiddleware< TRegister, TNewMethod, TMiddlewares, - TInputValidator + TInputValidator, + TNewStrict > } -export type ValidatorFn = < +export type ValidatorFn< + TRegister, + TMethod extends Method, + TMiddlewares, + TStrict extends ServerFnStrict, +> = ( + inputValidator: ConstrainValidator< + TRegister, + TMethod, + TInputValidator, + TStrict + >, +) => ServerFnAfterValidator< + TRegister, + TMethod, + TMiddlewares, TInputValidator, ->( - inputValidator: ConstrainValidator, -) => ServerFnAfterValidator + TStrict +> export interface ServerFnValidator< TRegister, TMethod extends Method, TMiddlewares, + TStrict extends ServerFnStrict, > { - inputValidator: ValidatorFn + inputValidator: ValidatorFn } export interface ServerFnAfterValidator< @@ -591,6 +668,7 @@ export interface ServerFnAfterValidator< TMethod extends Method, TMiddlewares, TInputValidator, + TStrict extends ServerFnStrict, > extends ServerFnWithTypes< @@ -598,16 +676,30 @@ export interface ServerFnAfterValidator< TMethod, TMiddlewares, TInputValidator, - undefined + undefined, + TStrict >, - ServerFnMiddleware, - ServerFnHandler {} + ServerFnMiddleware< + TRegister, + TMethod, + TMiddlewares, + TInputValidator, + TStrict + >, + ServerFnHandler< + TRegister, + TMethod, + TMiddlewares, + TInputValidator, + TStrict + > {} export interface ServerFnAfterTyper< TRegister, TMethod extends Method, TMiddlewares, TInputValidator, + TStrict extends ServerFnStrict, > extends ServerFnWithTypes< @@ -615,9 +707,16 @@ export interface ServerFnAfterTyper< TMethod, TMiddlewares, TInputValidator, - undefined + undefined, + TStrict >, - ServerFnHandler {} + ServerFnHandler< + TRegister, + TMethod, + TMiddlewares, + TInputValidator, + TStrict + > {} // Handler export interface ServerFnHandler< @@ -625,6 +724,7 @@ export interface ServerFnHandler< TMethod extends Method, TMiddlewares, TInputValidator, + TStrict extends ServerFnStrict, > { handler: ( fn?: ServerFn< @@ -632,23 +732,42 @@ export interface ServerFnHandler< TMethod, TMiddlewares, TInputValidator, - TNewResponse + TNewResponse, + TStrict >, ) => Fetcher } -export interface ServerFnBuilder +export interface ServerFnBuilder< + TRegister, + TMethod extends Method = 'GET', + TStrict extends ServerFnStrict = true, +> extends - ServerFnWithTypes, - ServerFnMiddleware, - ServerFnValidator, - ServerFnHandler { + ServerFnWithTypes< + TRegister, + TMethod, + undefined, + undefined, + undefined, + TStrict + >, + ServerFnMiddleware, + ServerFnValidator, + ServerFnHandler { + < + TNewMethod extends Method = TMethod, + TNewStrict extends ServerFnStrict = TStrict, + >( + options?: ServerFnOptions, + ): ServerFnBuilder options: ServerFnBaseOptions< TRegister, TMethod, unknown, undefined, - undefined + undefined, + TStrict > } @@ -658,25 +777,28 @@ export interface ServerFnWithTypes< in out TMiddlewares, in out TInputValidator, in out TResponse, + in out TStrict extends ServerFnStrict, > { '~types': ServerFnTypes< TRegister, TMethod, TMiddlewares, TInputValidator, - TResponse + TResponse, + TStrict > options: ServerFnBaseOptions< TRegister, TMethod, unknown, undefined, - undefined + undefined, + TStrict > [TSS_SERVER_FUNCTION_FACTORY]: true } -export type AnyServerFn = ServerFnWithTypes +export type AnyServerFn = ServerFnWithTypes export interface ServerFnTypes< in out TRegister, @@ -684,8 +806,10 @@ export interface ServerFnTypes< in out TMiddlewares, in out TInputValidator, in out TResponse, + in out TStrict extends ServerFnStrict, > { method: TMethod + strict: TStrict middlewares: TMiddlewares inputValidator: TInputValidator response: TResponse diff --git a/packages/start-client-core/src/index.tsx b/packages/start-client-core/src/index.tsx index 5b9289b5eb..f7835c9859 100644 --- a/packages/start-client-core/src/index.tsx +++ b/packages/start-client-core/src/index.tsx @@ -65,6 +65,10 @@ export type { FetcherBaseOptions, ServerFn, ServerFnCtx, + ServerFnOptions, + ServerFnStrict, + ServerFnStrictInput, + ServerFnStrictOutput, MiddlewareFn, ServerFnMiddlewareOptions, ServerFnMiddlewareResult, diff --git a/packages/start-client-core/src/tests/createServerFn.test-d.ts b/packages/start-client-core/src/tests/createServerFn.test-d.ts index 7cac3a3903..eabaca198b 100644 --- a/packages/start-client-core/src/tests/createServerFn.test-d.ts +++ b/packages/start-client-core/src/tests/createServerFn.test-d.ts @@ -518,6 +518,131 @@ test('createServerFn cannot return function', () => { }>() }) +test('createServerFn strict false can validate and return function', () => { + const fn = createServerFn({ method: 'GET', strict: false }) + .inputValidator((input: { func: () => 'input' }) => ({ + output: input.func(), + })) + .handler(({ data }) => { + expectTypeOf(data).toEqualTypeOf<{ output: 'input' }>() + + return { + func: () => 'func' as const, + } + }) + + expectTypeOf(fn).parameter(0).toEqualTypeOf<{ + data: { func: () => 'input' } + headers?: HeadersInit + signal?: AbortSignal + fetch?: CustomFetch + }>() + + expectTypeOf(fn({ data: { func: () => 'input' } })).toEqualTypeOf< + Promise<{ + func: () => 'func' + }> + >() +}) + +test('createServerFn strict false factory preserves strictness', () => { + const createServerFnWithoutSerializationCheck = createServerFn({ + strict: false, + }) + + const myServerFn = createServerFnWithoutSerializationCheck() + .inputValidator((input: { func: () => 'input' }) => ({ + output: input.func(), + })) + .handler(({ data }) => { + expectTypeOf(data).toEqualTypeOf<{ output: 'input' }>() + + return { + func: () => 'func' as const, + } + }) + + expectTypeOf(myServerFn).parameter(0).toEqualTypeOf<{ + data: { func: () => 'input' } + headers?: HeadersInit + signal?: AbortSignal + fetch?: CustomFetch + }>() + + expectTypeOf(myServerFn({ data: { func: () => 'input' } })).toEqualTypeOf< + Promise<{ + func: () => 'func' + }> + >() +}) + +test('createServerFn strict input false can validate function', () => { + const fn = createServerFn({ strict: { input: false } }) + .inputValidator((input: { func: () => 'input' }) => ({ + output: input.func(), + })) + .handler(({ data }) => { + expectTypeOf(data).toEqualTypeOf<{ output: 'input' }>() + + return { + value: 'serializable' as const, + } + }) + + expectTypeOf(fn).parameter(0).toEqualTypeOf<{ + data: { func: () => 'input' } + headers?: HeadersInit + signal?: AbortSignal + fetch?: CustomFetch + }>() + + expectTypeOf(fn({ data: { func: () => 'input' } })).toEqualTypeOf< + Promise<{ + value: 'serializable' + }> + >() +}) + +test('createServerFn strict output false can return function', () => { + const fn = createServerFn({ strict: { output: false } }).handler(() => ({ + func: () => 'func' as const, + })) + + expectTypeOf(fn()).toEqualTypeOf< + Promise<{ + func: () => 'func' + }> + >() + + const promiseFn = createServerFn({ strict: { output: false } }).handler(() => + Promise.resolve({ + func: () => 'func' as const, + }), + ) + + expectTypeOf(promiseFn()).toEqualTypeOf< + Promise<{ + func: () => 'func' + }> + >() +}) + +test('ServerFnReturnType skips serialization when strict is false', () => { + expectTypeOf< + ServerFnReturnType 'func' }, false> + >().toEqualTypeOf<{ + func: () => 'func' + }>() +}) + +test('ServerFnReturnType skips serialization when strict output is false', () => { + expectTypeOf< + ServerFnReturnType 'func' }, { output: false }> + >().toEqualTypeOf<{ + func: () => 'func' + }>() +}) + test('createServerFn cannot validate function', () => { const validator = createServerFn().inputValidator< (input: { func: () => 'string' }) => { output: 'string' } @@ -536,6 +661,25 @@ test('createServerFn cannot validate function', () => { >() }) +test('createServerFn strict output false still checks input', () => { + const validator = createServerFn({ + method: 'GET', + strict: { output: false }, + }).inputValidator<(input: { func: () => 'string' }) => { output: 'string' }> + + expectTypeOf(validator) + .parameter(0) + .toEqualTypeOf< + Constrain< + (input: { func: () => 'string' }) => { output: 'string' }, + Validator< + { func: SerializationError<'Function may not be serializable'> }, + any + > + > + >() +}) + test('createServerFn can validate Date', () => { const validator = createServerFn().inputValidator< (input: Date) => { output: 'string' }