diff --git a/.changeset/wet-nails-itch.md b/.changeset/wet-nails-itch.md new file mode 100644 index 000000000..ad4e73531 --- /dev/null +++ b/.changeset/wet-nails-itch.md @@ -0,0 +1,6 @@ +--- +"@orpc/server": minor +"@orpc/shared": minor +--- + +feat!: `.handler` become `.func`, caller accept FormData diff --git a/apps/content/content/docs/client/react.mdx b/apps/content/content/docs/client/react.mdx index f2871ab45..369ea6078 100644 --- a/apps/content/content/docs/client/react.mdx +++ b/apps/content/content/docs/client/react.mdx @@ -84,7 +84,7 @@ const router = { user: { list: os .input(z.object({ cursor: z.number(), limit: z.number() })) - .handler((input) => { + .func((input) => { return { nextCursor: input.cursor + input.limit, users: [] diff --git a/apps/content/content/docs/contract-first.mdx b/apps/content/content/docs/contract-first.mdx index b794e9953..cd026b128 100644 --- a/apps/content/content/docs/contract-first.mdx +++ b/apps/content/content/docs/contract-first.mdx @@ -95,7 +95,7 @@ export const authed /** require authed */ = os .contract(contract) export const router = pub.router({ - getting: pub.getting.handler((input, context, meta) => { + getting: pub.getting.func((input, context, meta) => { return { message: `Hello, ${input.name}!`, } @@ -103,7 +103,7 @@ export const router = pub.router({ post: { find: pub.post.find - .handler((input, context, meta) => { + .func((input, context, meta) => { return { id: 'example', title: 'example', @@ -111,7 +111,7 @@ export const router = pub.router({ } }), - create: authed.post.create.handler((input, context, meta) => { + create: authed.post.create.func((input, context, meta) => { return { id: 'example', title: input.title, diff --git a/apps/content/content/docs/contract/builder.mdx b/apps/content/content/docs/contract/builder.mdx index 7236ec79c..773e8da5e 100644 --- a/apps/content/content/docs/contract/builder.mdx +++ b/apps/content/content/docs/contract/builder.mdx @@ -88,7 +88,7 @@ export const authed /** require authed */ = base .contract(contract) export const router = pub.router({ - getting: pub.getting.handler((input, context, meta) => { + getting: pub.getting.func((input, context, meta) => { return { message: `Hello, ${input.name}!`, } @@ -96,7 +96,7 @@ export const router = pub.router({ post: { find: pub.post.find - .handler((input, context, meta) => { + .func((input, context, meta) => { return { id: 'example', title: 'example', @@ -104,7 +104,7 @@ export const router = pub.router({ } }), - create: authed.post.create.handler((input, context, meta) => { + create: authed.post.create.func((input, context, meta) => { return { id: 'example', title: input.title, diff --git a/apps/content/content/docs/index.mdx b/apps/content/content/docs/index.mdx index f66775256..d8aa60ad9 100644 --- a/apps/content/content/docs/index.mdx +++ b/apps/content/content/docs/index.mdx @@ -68,7 +68,7 @@ export const router = pub.router({ name: z.string(), }), ) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { return { message: `Hello, ${input.name}!`, } @@ -105,7 +105,7 @@ export const router = pub.router({ return result }) - .handler((input, context, meta) => { + .func((input, context, meta) => { return { id: 'example', title: 'example', @@ -121,7 +121,7 @@ export const router = pub.router({ thumb: oz.file().type('image/*'), }), ) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { input.thumb // file upload out of the box return { diff --git a/apps/content/content/docs/openapi/generator.mdx b/apps/content/content/docs/openapi/generator.mdx index 9518ef8ad..7608fd3d5 100644 --- a/apps/content/content/docs/openapi/generator.mdx +++ b/apps/content/content/docs/openapi/generator.mdx @@ -29,7 +29,7 @@ export const router = pub.router({ name: z.string(), }), ) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { return { message: `Hello, ${input.name}!`, } @@ -53,7 +53,7 @@ export const router = pub.router({ description: z.string(), }), ) - .handler((input, context, meta) => { + .func((input, context, meta) => { return { id: 'example', title: 'example', @@ -69,7 +69,7 @@ export const router = pub.router({ thumb: oz.file().type('image/*'), }), ) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { input.thumb // file upload out of the box return { diff --git a/apps/content/content/docs/server/advanced.mdx b/apps/content/content/docs/server/advanced.mdx index 15bc1be99..8faa8f720 100644 --- a/apps/content/content/docs/server/advanced.mdx +++ b/apps/content/content/docs/server/advanced.mdx @@ -9,13 +9,12 @@ description: Beyond the basics, oRPC offers more advanced features import { os, createRouterCaller } from '@orpc/server' const router = os.router({ - ping: os.handler(() => 'pong') + ping: os.func(() => 'pong') }) const caller = createRouterCaller({ router: router, context: {}, - validate: true // set false if you want bypass the validation input and output }) const result = await caller.ping(undefined) // result is 'pong' diff --git a/apps/content/content/docs/server/error-handling.mdx b/apps/content/content/docs/server/error-handling.mdx index 1e3fb32e6..51ef32432 100644 --- a/apps/content/content/docs/server/error-handling.mdx +++ b/apps/content/content/docs/server/error-handling.mdx @@ -19,7 +19,7 @@ const ping = os // do something on finish } }) - .handler((input, context, meta) => { + .func((input, context, meta) => { throw new ORPCError({ code: 'NOT_FOUND', message: 'Not found', diff --git a/apps/content/content/docs/server/file-upload.mdx b/apps/content/content/docs/server/file-upload.mdx index 9f1056d2d..39ede1dcb 100644 --- a/apps/content/content/docs/server/file-upload.mdx +++ b/apps/content/content/docs/server/file-upload.mdx @@ -20,7 +20,7 @@ import { z } from 'zod' export const uploadFile = os.input( z.object({ file: z.instanceof(File) }) -).handler(async (input) => { +).func(async (input) => { const file: File = input.file // Handle the file as needed, e.g., save to storage, process, etc. }) @@ -45,7 +45,7 @@ import { z } from 'zod' export const uploadFile = os.input( z.object({ file: oz.file().type('image/*') }) -).handler(async (input) => { +).func(async (input) => { const image: File = input.file // Process the image file as needed }) diff --git a/apps/content/content/docs/server/middleware.mdx b/apps/content/content/docs/server/middleware.mdx index b970be516..7597ee681 100644 --- a/apps/content/content/docs/server/middleware.mdx +++ b/apps/content/content/docs/server/middleware.mdx @@ -114,7 +114,7 @@ export const authed = pub } }) }) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { const _expect1: NonNullable = context.user const _expect2: string = context.say diff --git a/apps/content/content/docs/server/procedure.mdx b/apps/content/content/docs/server/procedure.mdx index 696576c4d..e8506996a 100644 --- a/apps/content/content/docs/server/procedure.mdx +++ b/apps/content/content/docs/server/procedure.mdx @@ -52,7 +52,7 @@ const findUser = pub }) // Define handler with business logic - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { // Implement your business logic here return { diff --git a/apps/content/content/docs/server/router.mdx b/apps/content/content/docs/server/router.mdx index b0e3b5b3b..4667dabc7 100644 --- a/apps/content/content/docs/server/router.mdx +++ b/apps/content/content/docs/server/router.mdx @@ -10,7 +10,7 @@ A router is a collection of procedures with utilities that help reduce code dupl ```ts twoslash import { os } from '@orpc/server' -const findUser = os.route({path: '/'}).handler(() => {}) +const findUser = os.route({path: '/'}).func(() => {}) export const appRouter = os.router({ user: { diff --git a/apps/content/content/docs/server/server-action.mdx b/apps/content/content/docs/server/server-action.mdx index fdc4ca372..04b4e9583 100644 --- a/apps/content/content/docs/server/server-action.mdx +++ b/apps/content/content/docs/server/server-action.mdx @@ -16,23 +16,32 @@ Server actions are automatically enabled for procedures that have either: ```ts twoslash "use server" -import { os } from '@orpc/server' +import { os, createProcedureCaller } from '@orpc/server' import { z } from 'zod' // This creates a server action since context is undefined const actionProcedure = os .input(z.object({ id: z.number() })) - .handler(() => 'result') + .func(() => 'result') // This also creates a server action since context is optional const optionalContextProcedure = os .context<{ auth: boolean } | undefined>() - .handler(() => 'result') + .func(() => 'result') // This won't be a server action since it requires context const regularProcedure = os .context<{ auth: boolean }>() - .handler(() => 'result') + .func(() => 'result') + +// But this will be a server action since it already provided context +const regularProcedure2 = createProcedureCaller({ + procedure: regularProcedure, + context: async () => { + // you can use headers, cookies, etc. here + return { auth: true } + }, +}) ``` ## Usage Examples @@ -59,7 +68,7 @@ export const createPost = os }), }), ) - .handler((input) => { + .func((input) => { redirect('/posts/new') }) ``` diff --git a/apps/content/examples/contract.ts b/apps/content/examples/contract.ts index c675158fe..edf4e8537 100644 --- a/apps/content/examples/contract.ts +++ b/apps/content/examples/contract.ts @@ -80,7 +80,7 @@ export const authed /** require authed */ = base .contract(contract) export const router = pub.router({ - getting: pub.getting.handler((input, context, meta) => { + getting: pub.getting.func((input, context, meta) => { return { message: `Hello, ${input.name}!`, } @@ -105,7 +105,7 @@ export const router = pub.router({ return result }) - .handler((input, context, meta) => { + .func((input, context, meta) => { return { id: 'example', title: 'example', @@ -113,7 +113,7 @@ export const router = pub.router({ } }), - create: authed.post.create.handler((input, context, meta) => { + create: authed.post.create.func((input, context, meta) => { return { id: 'example', title: input.title, diff --git a/apps/content/examples/middleware.ts b/apps/content/examples/middleware.ts index 6d3be666a..96ca2d78c 100644 --- a/apps/content/examples/middleware.ts +++ b/apps/content/examples/middleware.ts @@ -58,7 +58,7 @@ export const editPost = authed const _output = result.output return result }) - .handler(() => 'Edited') + .func(() => 'Edited') // // diff --git a/apps/content/examples/server-action.tsx b/apps/content/examples/server-action.tsx index f19dae455..fcc579a68 100644 --- a/apps/content/examples/server-action.tsx +++ b/apps/content/examples/server-action.tsx @@ -18,7 +18,7 @@ export const createPost = os }), }), ) - .handler((input) => { + .func((input) => { redirect('/posts/new') }) diff --git a/apps/content/examples/server.ts b/apps/content/examples/server.ts index 3b97e3b2f..6e7b65c90 100644 --- a/apps/content/examples/server.ts +++ b/apps/content/examples/server.ts @@ -25,7 +25,7 @@ export const router = pub.router({ name: z.string(), }), ) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { return { message: `Hello, ${input.name}!`, } @@ -66,7 +66,7 @@ export const router = pub.router({ return result }) - .handler((input, context, meta) => { + .func((input, context, meta) => { return { id: 'example', title: 'example', @@ -82,7 +82,7 @@ export const router = pub.router({ thumb: oz.file().type('image/*'), }), ) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { const _thumb = input.thumb // file upload out of the box return { diff --git a/packages/client/src/procedure.test.ts b/packages/client/src/procedure.test.ts index 58dd57b90..953339210 100644 --- a/packages/client/src/procedure.test.ts +++ b/packages/client/src/procedure.test.ts @@ -7,7 +7,7 @@ describe('createProcedureClient', () => { const schema = z.object({ value: z.string(), }) - const ping = os.input(schema).handler((_, __, { path }) => path) + const ping = os.input(schema).func((_, __, { path }) => path) const router = os.router({ ping, nested: { @@ -111,7 +111,7 @@ describe('createProcedureClient', () => { const router = os.router({ ping: os .input(z.object({ value: z.date() })) - .handler(input => input.value), + .func(input => input.value), }) const handler = createFetchHandler({ @@ -137,7 +137,7 @@ describe('createProcedureClient', () => { it('error include data', async () => { const router = os.router({ - ping: os.handler((input) => { + ping: os.func((input) => { throw new ORPCError({ code: 'BAD_GATEWAY', data: { @@ -180,14 +180,14 @@ describe('createProcedureClient', () => { describe('upload file', () => { const router = os.router({ - signal: os.input(z.instanceof(Blob)).handler((input) => { + signal: os.input(z.instanceof(Blob)).func((input) => { return input }), multiple: os .input( z.object({ first: z.instanceof(Blob), second: z.instanceof(Blob) }), ) - .handler((input) => { + .func((input) => { return input }), }) diff --git a/packages/client/src/procedure.ts b/packages/client/src/procedure.ts index 15ff6e837..da60019b8 100644 --- a/packages/client/src/procedure.ts +++ b/packages/client/src/procedure.ts @@ -16,11 +16,11 @@ import { ORPCDeserializer, ORPCSerializer } from '@orpc/transformer' export interface ProcedureClient< TInputSchema extends Schema, TOutputSchema extends Schema, - THandlerOutput extends SchemaOutput, + TFuncOutput extends SchemaOutput, > { ( input: SchemaInput, - ): Promise> + ): Promise> } export interface CreateProcedureClientOptions { @@ -50,10 +50,10 @@ export interface CreateProcedureClientOptions { export function createProcedureClient< TInputSchema extends Schema, TOutputSchema extends Schema, - THandlerOutput extends SchemaOutput, + TFuncOutput extends SchemaOutput, >( options: CreateProcedureClientOptions, -): ProcedureClient { +): ProcedureClient { const serializer = new ORPCSerializer() const deserializer = new ORPCDeserializer() diff --git a/packages/client/src/router.test.ts b/packages/client/src/router.test.ts index 253ce4675..e17f1a093 100644 --- a/packages/client/src/router.test.ts +++ b/packages/client/src/router.test.ts @@ -8,7 +8,7 @@ describe('createRouterClient', () => { const schema = z.object({ value: z.string(), }) - const ping = os.input(schema).handler((_, __, { path }) => path) + const ping = os.input(schema).func((_, __, { path }) => path) const router = os.router({ ping, nested: { @@ -65,9 +65,9 @@ describe('createRouterClient', () => { const schema = z.object({ value: z.string(), }) - const ping = os.input(schema).handler(() => '') - const pong = os.output(schema).handler(() => ({ value: 'string' })) - const peng = os.route({}).handler(() => ({ age: 1244 })) + const ping = os.input(schema).func(() => '') + const pong = os.output(schema).func(() => ({ value: 'string' })) + const peng = os.route({}).func(() => ({ age: 1244 })) const router = os.router({ ping, @@ -123,7 +123,7 @@ describe('createRouterClient', () => { const router = os.router({ ping: os .input(z.object({ value: z.date() })) - .handler(input => input.value), + .func(input => input.value), }) const handler = createFetchHandler({ diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts index d94f29a26..8fc186376 100644 --- a/packages/client/src/router.ts +++ b/packages/client/src/router.ts @@ -26,9 +26,9 @@ export type RouterClientWithRouter> = { any, infer UInputSchema, infer UOutputSchema, - infer UHandlerOutput + infer UFuncOutput > - ? ProcedureClient + ? ProcedureClient : TRouter[K] extends Router ? RouterClientWithRouter : never diff --git a/packages/react/src/general-hooks.ts b/packages/react/src/general-hooks.ts index b7b4aab67..3a10a913a 100644 --- a/packages/react/src/general-hooks.ts +++ b/packages/react/src/general-hooks.ts @@ -16,7 +16,7 @@ import { getMutationKeyFromPath, getQueryKeyFromPath } from './tanstack-key' export interface GeneralHooks< TInputSchema extends Schema, TOutputSchema extends Schema, - THandlerOutput extends SchemaOutput, + TFuncOutput extends SchemaOutput, > { useIsFetching: ( filers?: ORPCQueryFilters>>, @@ -25,7 +25,7 @@ export interface GeneralHooks< useMutationState: < UResult = MutationState< - SchemaOutput, + SchemaOutput, DefaultError, SchemaInput >, @@ -33,7 +33,7 @@ export interface GeneralHooks< filters?: SetOptional select?: ( mutation: Mutation< - SchemaOutput, + SchemaOutput, DefaultError, SchemaInput >, @@ -56,11 +56,11 @@ export interface CreateGeneralHooksOptions { export function createGeneralHooks< TInputSchema extends Schema = undefined, TOutputSchema extends Schema = undefined, - THandlerOutput extends + TFuncOutput extends SchemaOutput = SchemaOutput, >( options: CreateGeneralHooksOptions, -): GeneralHooks { +): GeneralHooks { return { useIsFetching(filters) { const { queryType, input, ...rest } = filters ?? {} diff --git a/packages/react/src/general-utils.ts b/packages/react/src/general-utils.ts index 4caed91f6..e7efd2a72 100644 --- a/packages/react/src/general-utils.ts +++ b/packages/react/src/general-utils.ts @@ -26,14 +26,14 @@ import { getMutationKeyFromPath, getQueryKeyFromPath } from './tanstack-key' export interface GeneralUtils< TInputSchema extends Schema, TOutputSchema extends Schema, - THandlerOutput extends SchemaOutput, + TFuncOutput extends SchemaOutput, > { getQueriesData: ( filters?: OmitKeyof< ORPCQueryFilters>>, 'queryType' >, - ) => [QueryKey, SchemaOutput | undefined][] + ) => [QueryKey, SchemaOutput | undefined][] getInfiniteQueriesData: ( filters?: OmitKeyof< ORPCQueryFilters>>, @@ -43,7 +43,7 @@ export interface GeneralUtils< QueryKey, | undefined | InfiniteData< - SchemaOutput, + SchemaOutput, SchemaInput['cursor'] >, ][] @@ -54,11 +54,11 @@ export interface GeneralUtils< 'queryType' >, updater: Updater< - SchemaOutput | undefined, - SchemaOutput | undefined + SchemaOutput | undefined, + SchemaOutput | undefined >, options?: SetDataOptions, - ) => [QueryKey, SchemaOutput | undefined][] + ) => [QueryKey, SchemaOutput | undefined][] setInfiniteQueriesData: ( filters: OmitKeyof< ORPCQueryFilters>>, @@ -66,12 +66,12 @@ export interface GeneralUtils< >, updater: Updater< | InfiniteData< - SchemaOutput, + SchemaOutput, SchemaInput['cursor'] > | undefined, | InfiniteData< - SchemaOutput, + SchemaOutput, SchemaInput['cursor'] > | undefined @@ -81,7 +81,7 @@ export interface GeneralUtils< QueryKey, | undefined | InfiniteData< - SchemaOutput, + SchemaOutput, SchemaInput['cursor'] >, ][] @@ -119,7 +119,7 @@ export interface GeneralUtils< 'input' | 'queryKey' >, ) => OmitKeyof< - QueryObserverOptions>, + QueryObserverOptions>, 'queryKey' > getInfiniteQueryDefaults: ( @@ -129,10 +129,10 @@ export interface GeneralUtils< >, ) => OmitKeyof< QueryObserverOptions< - SchemaOutput, + SchemaOutput, DefaultError, - SchemaOutput, - InfiniteData>, + SchemaOutput, + InfiniteData>, QueryKey, SchemaInput['cursor'] >, @@ -142,7 +142,7 @@ export interface GeneralUtils< setQueryDefaults: ( options: Partial< OmitKeyof< - QueryObserverOptions>, + QueryObserverOptions>, 'queryKey' > >, @@ -155,10 +155,10 @@ export interface GeneralUtils< options: Partial< OmitKeyof< QueryObserverOptions< - SchemaOutput, + SchemaOutput, DefaultError, - SchemaOutput, - InfiniteData>, + SchemaOutput, + InfiniteData>, QueryKey, SchemaInput['cursor'] >, @@ -174,14 +174,14 @@ export interface GeneralUtils< getMutationDefaults: ( filters?: Pick, ) => MutationObserverOptions< - SchemaOutput, + SchemaOutput, DefaultError, SchemaInput > setMutationDefaults: ( options: OmitKeyof< MutationObserverOptions< - SchemaOutput, + SchemaOutput, DefaultError, SchemaInput >, @@ -205,11 +205,11 @@ export interface CreateGeneralUtilsOptions { export function createGeneralUtils< TInputSchema extends Schema = undefined, TOutputSchema extends Schema = undefined, - THandlerOutput extends + TFuncOutput extends SchemaOutput = SchemaOutput, >( options: CreateGeneralUtilsOptions, -): GeneralUtils { +): GeneralUtils { return { getQueriesData(filters) { const { input, ...rest } = filters ?? {} diff --git a/packages/react/src/procedure-hooks.ts b/packages/react/src/procedure-hooks.ts index c16e31ab9..aec481f81 100644 --- a/packages/react/src/procedure-hooks.ts +++ b/packages/react/src/procedure-hooks.ts @@ -36,13 +36,13 @@ import { getMutationKeyFromPath, getQueryKeyFromPath } from './tanstack-key' export interface ProcedureHooks< TInputSchema extends Schema, TOutputSchema extends Schema, - THandlerOutput extends SchemaOutput, + TFuncOutput extends SchemaOutput, > { - useQuery: >( + useQuery: >( input: SchemaInput, options?: SetOptional< UseQueryOptions< - SchemaOutput, + SchemaOutput, DefaultError, USelectData >, @@ -51,17 +51,17 @@ export interface ProcedureHooks< ) => UseQueryResult useInfiniteQuery: < USelectData = InfiniteData< - SchemaOutput, + SchemaOutput, SchemaInput['cursor'] >, >( options: PartialOnUndefinedDeep< SetOptional< UseInfiniteQueryOptions< - SchemaOutput, + SchemaOutput, DefaultError, USelectData, - SchemaOutput, + SchemaOutput, QueryKey, SchemaInput['cursor'] >, @@ -72,11 +72,11 @@ export interface ProcedureHooks< >, ) => UseInfiniteQueryResult - useSuspenseQuery: >( + useSuspenseQuery: >( input: SchemaInput, options?: SetOptional< UseSuspenseQueryOptions< - SchemaOutput, + SchemaOutput, DefaultError, USelectData >, @@ -85,17 +85,17 @@ export interface ProcedureHooks< ) => UseSuspenseQueryResult useSuspenseInfiniteQuery: < USelectData = InfiniteData< - SchemaOutput, + SchemaOutput, SchemaInput['cursor'] >, >( options: PartialOnUndefinedDeep< SetOptional< UseSuspenseInfiniteQueryOptions< - SchemaOutput, + SchemaOutput, DefaultError, USelectData, - SchemaOutput, + SchemaOutput, QueryKey, SchemaInput['cursor'] >, @@ -108,15 +108,15 @@ export interface ProcedureHooks< usePrefetchQuery: ( input: SchemaInput, - options?: FetchQueryOptions>, + options?: FetchQueryOptions>, ) => void usePrefetchInfiniteQuery: ( options: PartialOnUndefinedDeep< SetOptional< FetchInfiniteQueryOptions< - SchemaOutput, + SchemaOutput, DefaultError, - SchemaOutput, + SchemaOutput, QueryKey, SchemaInput['cursor'] >, @@ -130,14 +130,14 @@ export interface ProcedureHooks< useMutation: ( options?: SetOptional< UseMutationOptions< - SchemaOutput, + SchemaOutput, DefaultError, SchemaInput >, 'mutationFn' | 'mutationKey' >, ) => UseMutationResult< - SchemaOutput, + SchemaOutput, DefaultError, SchemaInput > @@ -157,11 +157,11 @@ export interface CreateProcedureHooksOptions { export function createProcedureHooks< TInputSchema extends Schema = undefined, TOutputSchema extends Schema = undefined, - THandlerOutput extends + TFuncOutput extends SchemaOutput = SchemaOutput, >( options: CreateProcedureHooksOptions, -): ProcedureHooks { +): ProcedureHooks { return { [orpcPathSymbol as any]: options.path, diff --git a/packages/react/src/procedure-utils.ts b/packages/react/src/procedure-utils.ts index 25ca704db..52dda17a3 100644 --- a/packages/react/src/procedure-utils.ts +++ b/packages/react/src/procedure-utils.ts @@ -20,22 +20,22 @@ import { getQueryKeyFromPath } from './tanstack-key' export interface ProcedureUtils< TInputSchema extends Schema, TOutputSchema extends Schema, - THandlerOutput extends SchemaOutput, + TFuncOutput extends SchemaOutput, > { fetchQuery: ( input: SchemaInput, options?: SetOptional< - FetchQueryOptions>, + FetchQueryOptions>, 'queryKey' | 'queryFn' >, - ) => Promise> + ) => Promise> fetchInfiniteQuery: ( options: PartialOnUndefinedDeep< SetOptional< FetchInfiniteQueryOptions< - SchemaOutput, + SchemaOutput, DefaultError, - SchemaOutput, + SchemaOutput, QueryKey, SchemaInput['cursor'] >, @@ -46,7 +46,7 @@ export interface ProcedureUtils< >, ) => Promise< InfiniteData< - SchemaOutput, + SchemaOutput, SchemaInput['cursor'] > > @@ -54,7 +54,7 @@ export interface ProcedureUtils< prefetchQuery: ( input: SchemaInput, options?: SetOptional< - FetchQueryOptions>, + FetchQueryOptions>, 'queryKey' | 'queryFn' >, ) => Promise @@ -62,9 +62,9 @@ export interface ProcedureUtils< options: PartialOnUndefinedDeep< SetOptional< FetchInfiniteQueryOptions< - SchemaOutput, + SchemaOutput, DefaultError, - SchemaOutput, + SchemaOutput, QueryKey, SchemaInput['cursor'] >, @@ -77,11 +77,11 @@ export interface ProcedureUtils< getQueryData: ( input: SchemaInput, - ) => SchemaOutput | undefined + ) => SchemaOutput | undefined getInfiniteQueryData: ( input: SchemaInputForInfiniteQuery, ) => | InfiniteData< - SchemaOutput, + SchemaOutput, SchemaInput['cursor'] > | undefined @@ -89,17 +89,17 @@ export interface ProcedureUtils< ensureQueryData: ( input: SchemaInput, options?: SetOptional< - EnsureQueryDataOptions>, + EnsureQueryDataOptions>, 'queryFn' | 'queryKey' >, - ) => Promise> + ) => Promise> ensureInfiniteQueryData: ( options: PartialOnUndefinedDeep< SetOptional< EnsureInfiniteQueryDataOptions< - SchemaOutput, + SchemaOutput, DefaultError, - SchemaOutput, + SchemaOutput, QueryKey, SchemaInput['cursor'] >, @@ -110,19 +110,19 @@ export interface ProcedureUtils< >, ) => Promise< InfiniteData< - SchemaOutput, + SchemaOutput, SchemaInput['cursor'] > > getQueryState: ( input: SchemaInput, - ) => QueryState> | undefined + ) => QueryState> | undefined getInfiniteQueryState: ( input: SchemaInputForInfiniteQuery, ) => | QueryState< InfiniteData< - SchemaOutput, + SchemaOutput, SchemaInput['cursor'] > > @@ -131,28 +131,28 @@ export interface ProcedureUtils< setQueryData: ( input: SchemaInput, updater: Updater< - SchemaOutput | undefined, - SchemaOutput | undefined + SchemaOutput | undefined, + SchemaOutput | undefined >, options?: SetDataOptions, - ) => SchemaOutput | undefined + ) => SchemaOutput | undefined setInfiniteQueryData: ( input: SchemaInputForInfiniteQuery, updater: Updater< | InfiniteData< - SchemaOutput, + SchemaOutput, SchemaInput['cursor'] > | undefined, | InfiniteData< - SchemaOutput, + SchemaOutput, SchemaInput['cursor'] > | undefined >, options?: SetDataOptions, ) => | InfiniteData< - SchemaOutput, + SchemaOutput, SchemaInput['cursor'] > | undefined @@ -161,10 +161,10 @@ export interface ProcedureUtils< export interface CreateProcedureUtilsOptions< TInputSchema extends Schema = undefined, TOutputSchema extends Schema = undefined, - THandlerOutput extends + TFuncOutput extends SchemaOutput = SchemaOutput, > { - client: ProcedureClient + client: ProcedureClient queryClient: QueryClient /** @@ -176,14 +176,14 @@ export interface CreateProcedureUtilsOptions< export function createProcedureUtils< TInputSchema extends Schema, TOutputSchema extends Schema, - THandlerOutput extends SchemaOutput, + TFuncOutput extends SchemaOutput, >( options: CreateProcedureUtilsOptions< TInputSchema, TOutputSchema, - THandlerOutput + TFuncOutput >, -): ProcedureUtils { +): ProcedureUtils { return { fetchQuery(input, options_) { return options.queryClient.fetchQuery({ diff --git a/packages/react/src/react-hooks.ts b/packages/react/src/react-hooks.ts index a0ec2d607..235460489 100644 --- a/packages/react/src/react-hooks.ts +++ b/packages/react/src/react-hooks.ts @@ -21,8 +21,8 @@ export type ORPCHooksWithContractRouter = { } & GeneralHooks export type ORPCHooksWithRouter> = { - [K in keyof TRouter]: TRouter[K] extends Procedure - ? ProcedureHooks & GeneralHooks + [K in keyof TRouter]: TRouter[K] extends Procedure + ? ProcedureHooks & GeneralHooks : TRouter[K] extends Router ? ORPCHooksWithRouter : never diff --git a/packages/react/src/react-utils.ts b/packages/react/src/react-utils.ts index 008ccc1ef..4419bdb6d 100644 --- a/packages/react/src/react-utils.ts +++ b/packages/react/src/react-utils.ts @@ -18,8 +18,8 @@ export type ORPCUtilsWithContractRouter = { } & GeneralUtils export type ORPCUtilsWithRouter> = { - [K in keyof TRouter]: TRouter[K] extends Procedure - ? ProcedureUtils & GeneralUtils + [K in keyof TRouter]: TRouter[K] extends Procedure + ? ProcedureUtils & GeneralUtils : TRouter[K] extends Router ? ORPCUtilsWithRouter : never diff --git a/packages/react/src/use-queries/builder.ts b/packages/react/src/use-queries/builder.ts index df86a1858..2a62e78f5 100644 --- a/packages/react/src/use-queries/builder.ts +++ b/packages/react/src/use-queries/builder.ts @@ -25,23 +25,23 @@ type UseQueryOptionsForUseQueries< export interface UseQueriesBuilder< TInputSchema extends Schema, TOutputSchema extends Schema, - THandlerOutput extends SchemaOutput, + TFuncOutput extends SchemaOutput, > { ( input: SchemaInput, options?: SetOptional< - UseQueryOptionsForUseQueries>, + UseQueryOptionsForUseQueries>, 'queryFn' | 'queryKey' >, - ): UseQueryOptionsForUseQueries> + ): UseQueryOptionsForUseQueries> } export interface CreateUseQueriesBuilderOptions< TInputSchema extends Schema, TOutputSchema extends Schema, - THandlerOutput extends SchemaOutput, + TFuncOutput extends SchemaOutput, > { - client: ProcedureClient + client: ProcedureClient /** * The path of procedure on server @@ -52,15 +52,15 @@ export interface CreateUseQueriesBuilderOptions< export function createUseQueriesBuilder< TInputSchema extends Schema = undefined, TOutputSchema extends Schema = undefined, - THandlerOutput extends + TFuncOutput extends SchemaOutput = SchemaOutput, >( options: CreateUseQueriesBuilderOptions< TInputSchema, TOutputSchema, - THandlerOutput + TFuncOutput >, -): UseQueriesBuilder { +): UseQueriesBuilder { return (input, options_) => { return { queryKey: getQueryKeyFromPath(options.path, { input, type: 'query' }), diff --git a/packages/react/src/use-queries/builders.ts b/packages/react/src/use-queries/builders.ts index 2d736d398..cf6a280de 100644 --- a/packages/react/src/use-queries/builders.ts +++ b/packages/react/src/use-queries/builders.ts @@ -34,9 +34,9 @@ export type UseQueriesBuildersWithRouter> = { any, infer UInputSchema, infer UOutputSchema, - infer UHandlerOutput + infer UFuncOutput > - ? UseQueriesBuilder + ? UseQueriesBuilder : TRouter[K] extends Router ? UseQueriesBuildersWithRouter : never diff --git a/packages/react/tests/orpc.tsx b/packages/react/tests/orpc.tsx index ea850b70e..57c20cd44 100644 --- a/packages/react/tests/orpc.tsx +++ b/packages/react/tests/orpc.tsx @@ -8,7 +8,7 @@ import { createORPCReact } from '../src' export const orpcServer = os -export const ping = orpcServer.handler(() => 'pong') +export const ping = orpcServer.func(() => 'pong') export const UserSchema = z .object({ data: z.object({ id: z.string(), name: z.string() }) }) @@ -20,7 +20,7 @@ export const UserFindInputSchema = z export const userFind = orpcServer .input(UserFindInputSchema) .output(UserSchema) - .handler((input) => { + .func((input) => { return { data: { id: input.data.id, @@ -46,7 +46,7 @@ export const UserListOutputSchema = z export const userList = orpcServer .input(UserListInputSchema) .output(UserListOutputSchema) - .handler((input) => { + .func((input) => { return { data: { nextCursor: input.data.cursor + 2, @@ -74,7 +74,7 @@ export const UserCreateInputSchema = z export const userCreate = orpcServer .input(UserCreateInputSchema) .output(UserSchema) - .handler((input) => { + .func((input) => { return { data: { id: '28aa6286-48e9-4f23-adea-3486c86acd55', diff --git a/packages/server/src/adapters/fetch.test.ts b/packages/server/src/adapters/fetch.test.ts index a7babeceb..c4e3a6103 100644 --- a/packages/server/src/adapters/fetch.test.ts +++ b/packages/server/src/adapters/fetch.test.ts @@ -6,13 +6,13 @@ import { ORPCError, os } from '..' import { createFetchHandler } from './fetch' const router = os.router({ - throw: os.handler(() => { + throw: os.func(() => { throw new Error('test') }), - ping: os.handler(() => { + ping: os.func(() => { return 'ping' }), - ping2: os.route({ method: 'GET', path: '/ping2' }).handler(() => { + ping2: os.route({ method: 'GET', path: '/ping2' }).func(() => { return 'ping2' }), }) @@ -22,10 +22,10 @@ const handler = createFetchHandler({ router }) describe('simple', () => { const osw = os.context<{ auth?: boolean }>() const router = osw.router({ - ping: osw.handler(async () => 'pong'), + ping: osw.func(async () => 'pong'), ping2: osw .route({ method: 'GET', path: '/ping2' }) - .handler(async () => 'pong2'), + .func(async () => 'pong2'), }) const handler = createFetchHandler({ router, @@ -37,7 +37,7 @@ describe('simple', () => { request: new Request('http://localhost/orpc/ping', { method: 'POST', }), - context: { auth: true }, + context: () => ({ auth: true }), }) expect(response.status).toBe(200) @@ -103,7 +103,7 @@ describe('procedure throw error', () => { it('orpc error', async () => { const router = os.router({ - ping: os.handler(() => { + ping: os.func(() => { throw new ORPCError({ code: 'TIMEOUT' }) }), }) @@ -124,7 +124,7 @@ describe('procedure throw error', () => { it('orpc error with data', async () => { const router = os.router({ - ping: os.handler(() => { + ping: os.func(() => { throw new ORPCError({ code: 'PAYLOAD_TOO_LARGE', message: 'test', @@ -150,14 +150,14 @@ describe('procedure throw error', () => { it('orpc error with custom status', async () => { const router = os.router({ - ping: os.handler(() => { + ping: os.func(() => { throw new ORPCError({ code: 'PAYLOAD_TOO_LARGE', status: 100, }) }), - ping2: os.handler(() => { + ping2: os.func(() => { throw new ORPCError({ code: 'PAYLOAD_TOO_LARGE', status: 488, @@ -195,7 +195,7 @@ describe('procedure throw error', () => { ping: os .input(z.object({})) .output(z.string()) - .handler(() => { + .func(() => { return 'unnoq' }), }) @@ -228,7 +228,7 @@ describe('procedure throw error', () => { ping: os .input(z.string()) .output(z.string()) - .handler(() => { + .func(() => { return 12344 as any }), }) @@ -332,14 +332,14 @@ describe('hooks', () => { describe('file upload', () => { const router = os.router({ - signal: os.input(z.instanceof(Blob)).handler((input) => { + signal: os.input(z.instanceof(Blob)).func((input) => { return input }), multiple: os .input( z.object({ first: z.instanceof(Blob), second: z.instanceof(Blob) }), ) - .handler((input) => { + .func((input) => { return input }), }) @@ -416,7 +416,7 @@ describe('file upload', () => { describe('accept header', () => { const router = os.router({ - ping: os.handler(async () => 'pong'), + ping: os.func(async () => 'pong'), }) const handler = createFetchHandler({ router, @@ -525,7 +525,7 @@ describe('dynamic params', () => { file: oz.file(), }), ) - .handler(input => input), + .func(input => input), find: os .route({ @@ -537,7 +537,7 @@ describe('dynamic params', () => { id: z.number(), }), ) - .handler(input => input), + .func(input => input), }) const handlers = [ @@ -591,7 +591,7 @@ describe('can control method on POST request', () => { file: oz.file(), }), ) - .handler(input => input), + .func(input => input), }) const handlers = [ diff --git a/packages/server/src/adapters/fetch.ts b/packages/server/src/adapters/fetch.ts index af3b5647a..7c9bd7ff1 100644 --- a/packages/server/src/adapters/fetch.ts +++ b/packages/server/src/adapters/fetch.ts @@ -3,6 +3,7 @@ import type { PartialOnUndefinedDeep, Promisable, + PromisableValue, } from '@orpc/shared' import type { Router } from '../router' import { @@ -15,6 +16,7 @@ import { get, isPlainObject, mapValues, + resolvePromisableValue, trim, } from '@orpc/shared' import { ORPCError } from '@orpc/shared/error' @@ -91,6 +93,8 @@ export function createFetchHandler>( ? new ORPCSerializer() : new OpenAPISerializer({ accept }) + const context = await resolvePromisableValue(requestOptions.context) + const handler = async () => { const url = new URL(requestOptions.request.url) const pathname = `/${trim(url.pathname.replace(requestOptions.prefix ?? '', ''), '/')}` @@ -196,9 +200,7 @@ export function createFetchHandler>( })() const caller = createProcedureCaller({ - context: requestOptions.context, - internal: false, - validate: true, + context, procedure, path, }) @@ -214,7 +216,7 @@ export function createFetchHandler>( } try { - return await options.hooks?.(requestOptions.context as any, { + return await options.hooks?.(context as any, { next: handler, response: response => response, }) ?? await handler() @@ -268,7 +270,9 @@ export type FetchHandlerOptions> = { /** * The context used to handle the request. */ - context: TRouter extends Router ? UContext : never + context: PromisableValue< + TRouter extends Router ? UContext : never + > }> export interface FetchHandler> { diff --git a/packages/server/src/builder.test.ts b/packages/server/src/builder.test.ts index 2f05a7ef5..9679436bc 100644 --- a/packages/server/src/builder.test.ts +++ b/packages/server/src/builder.test.ts @@ -74,7 +74,7 @@ describe('use middleware', () => { return { postId: '1' } }, ) - .handler((_, context) => { + .func((_, context) => { expectTypeOf(context).toMatchTypeOf<{ user: string }>() }) }) @@ -145,7 +145,7 @@ it('router method', () => { // Because of the router keyword is special, we can't use instanceof expect(osw.router.zz$pi.contract).toEqual(userFindContract) expect( - osw.router.handler(() => { + osw.router.func(() => { return { name: '' } }), ).toSatisfy(isProcedure) @@ -286,7 +286,7 @@ describe('handler method', () => { it('without middlewares', () => { const osw = os.context<{ auth: boolean }>() - const procedure = osw.handler((input, context, meta) => { + const procedure = osw.func((input, context, meta) => { expectTypeOf(input).toEqualTypeOf() expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>() expectTypeOf(meta).toEqualTypeOf() @@ -317,7 +317,7 @@ describe('handler method', () => { const osw = os.context<{ auth: boolean }>().use(mid) - const procedure = osw.handler((input, context, meta) => { + const procedure = osw.func((input, context, meta) => { expectTypeOf(input).toEqualTypeOf() expectTypeOf(context).toMatchTypeOf<{ auth: boolean }>() expectTypeOf(meta).toEqualTypeOf() diff --git a/packages/server/src/builder.ts b/packages/server/src/builder.ts index c847fc649..d1afe5e34 100644 --- a/packages/server/src/builder.ts +++ b/packages/server/src/builder.ts @@ -20,7 +20,7 @@ import { import { type DecoratedProcedure, decorateProcedure, - type ProcedureHandler, + type ProcedureFunc, } from './procedure' import { ProcedureBuilder } from './procedure-builder' import { ProcedureImplementer } from './procedure-implementer' @@ -137,20 +137,20 @@ export class Builder { /** * Convert to Procedure */ - handler( - handler: ProcedureHandler< + func( + func: ProcedureFunc< TContext, TExtraContext, undefined, undefined, - UHandlerOutput + UFuncOutput >, ): DecoratedProcedure< TContext, TExtraContext, undefined, undefined, - UHandlerOutput + UFuncOutput > { return decorateProcedure({ zz$p: { @@ -159,7 +159,7 @@ export class Builder { InputSchema: undefined, OutputSchema: undefined, }), - handler, + func, }, }) } diff --git a/packages/server/src/middleware.test.ts b/packages/server/src/middleware.test.ts index 280ce25b7..b76e3ce97 100644 --- a/packages/server/src/middleware.test.ts +++ b/packages/server/src/middleware.test.ts @@ -249,7 +249,7 @@ it('middleware can output', async () => { mid2Called = true return meta.output('from middleware 2') }) - .handler(() => { + .func(() => { handlerCalled = true return 'from handler' }) diff --git a/packages/server/src/procedure-builder.test.ts b/packages/server/src/procedure-builder.test.ts index 45c1e47a4..effe320ef 100644 --- a/packages/server/src/procedure-builder.test.ts +++ b/packages/server/src/procedure-builder.test.ts @@ -163,7 +163,7 @@ describe('use middleware', () => { describe('handler', () => { it('infer types', () => { - const handler = builder.handler((input, context, meta) => { + const handler = builder.func((input, context, meta) => { expectTypeOf(input).toEqualTypeOf() expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>() expectTypeOf(meta).toEqualTypeOf() @@ -196,7 +196,7 @@ describe('handler', () => { const handler = builder .use(mid1) .use(mid2) - .handler((input, context, meta) => { + .func((input, context, meta) => { expectTypeOf(input).toEqualTypeOf() expectTypeOf(context).toEqualTypeOf< { userId: string } & { auth: boolean } diff --git a/packages/server/src/procedure-builder.ts b/packages/server/src/procedure-builder.ts index 6efcae3cd..52a257adc 100644 --- a/packages/server/src/procedure-builder.ts +++ b/packages/server/src/procedure-builder.ts @@ -11,7 +11,7 @@ import { import { type DecoratedProcedure, decorateProcedure, - type ProcedureHandler, + type ProcedureFunc, } from './procedure' import { ProcedureImplementer } from './procedure-implementer' @@ -132,26 +132,26 @@ export class ProcedureBuilder< * Convert to Procedure */ - handler>( - handler: ProcedureHandler< + func>( + func: ProcedureFunc< TContext, TExtraContext, TInputSchema, TOutputSchema, - UHandlerOutput + UFuncOutput >, ): DecoratedProcedure< TContext, TExtraContext, TInputSchema, TOutputSchema, - UHandlerOutput + UFuncOutput > { return decorateProcedure({ zz$p: { middlewares: this.zz$pb.middlewares, contract: this.zz$pb.contract, - handler, + func, }, }) } diff --git a/packages/server/src/procedure-caller.test.ts b/packages/server/src/procedure-caller.test.ts index ed68fefbc..41d28a2b7 100644 --- a/packages/server/src/procedure-caller.test.ts +++ b/packages/server/src/procedure-caller.test.ts @@ -2,17 +2,15 @@ import { z } from 'zod' import { createProcedureCaller, os } from '.' describe('createProcedureCaller', () => { - let internal = false - let path = ['ping'] - let context = { auth: true } + const path = ['ping'] + const context = { auth: true } const osw = os.context<{ auth?: boolean }>() const procedure = osw .input(z.object({ value: z.string().transform(v => Number(v)) })) .output(z.object({ value: z.number().transform(v => v.toString()) })) - .handler((input, context, meta) => { + .func((input, context, meta) => { expect(context).toEqual(context) - expect(meta.internal).toBe(internal) expect(meta.path).toBe(path) return input @@ -34,8 +32,7 @@ describe('createProcedureCaller', () => { it('with validate', async () => { const caller = createProcedureCaller({ procedure, - context, - internal, + context: async () => context, path, }) @@ -53,41 +50,14 @@ describe('createProcedureCaller', () => { ) }) - it('without validate', async () => { - internal = true - path = [] - context = { auth: false } - - const caller = createProcedureCaller({ - procedure, - context, - internal, - path, - validate: false, - }) - - expectTypeOf(caller).toMatchTypeOf< - (input: { value: number }) => Promise<{ - value: number - }> - >() - - expect(await caller({ value: 123 })).toEqual({ value: 123 }) - - // @ts-expect-error it's not validate so bellow still works - expect(await caller({ value: '123' })).toEqual({ value: '123' }) - }) - it('without validate and schema', () => { - const procedure = osw.handler(() => { + const procedure = osw.func(() => { return { value: true } }) const caller = createProcedureCaller({ procedure, context, - internal, - validate: false, }) expectTypeOf(caller).toMatchTypeOf< @@ -146,7 +116,7 @@ describe('createProcedureCaller', () => { .input(z.object({ id: z.string() })) .use(mid1) .use(mid2) - .handler((input, context, meta) => { + .func((input, context, meta) => { expect(context).toEqual({ userId: '1', auth: false }) expect(ref.value).toBe(2) @@ -162,4 +132,25 @@ describe('createProcedureCaller', () => { expect(caller({ id: '1' })).resolves.toEqual('pong') }) + + it('accept form data', async () => { + const ping = osw + .input(z.object({ id: z.number() })) + .output(z.object({ id: z.number() })) + .func((input, context, meta) => { + expect(context).toEqual(context) + + return input + }) + + const caller = createProcedureCaller({ + procedure: ping, + context, + }) + + const form = new FormData() + form.append('id', '1') + + expect(await caller(form)).toEqual({ id: 1 }) + }) }) diff --git a/packages/server/src/procedure-caller.ts b/packages/server/src/procedure-caller.ts index 9db2ad942..c87dc7a73 100644 --- a/packages/server/src/procedure-caller.ts +++ b/packages/server/src/procedure-caller.ts @@ -2,84 +2,78 @@ import type { SchemaInput, SchemaOutput } from '@orpc/contract' import type { MiddlewareMeta } from './middleware' import type { Procedure } from './procedure' import type { Context } from './types' +import { type PromisableValue, resolvePromisableValue } from '@orpc/shared' import { ORPCError } from '@orpc/shared/error' +import { OpenAPIDeserializer } from '@orpc/transformer' import { mergeContext } from './utils' export interface CreateProcedureCallerOptions< TProcedure extends Procedure, - TValidate extends boolean, > { procedure: TProcedure /** * The context used when calling the procedure. */ - context: TProcedure extends Procedure - ? UContext - : never + context: PromisableValue< + TProcedure extends Procedure + ? UContext + : never + > /** * This is helpful for logging and analytics. - */ - path?: string[] - - /** - * This flag helpful when you want bypass some logics not necessary to internal server calls. * - * @default true + * @internal */ - internal?: boolean - - /** - * Indicate whether validate input and output. - * - * @default true - */ - validate?: TValidate + path?: string[] } export type ProcedureCaller< TProcedure extends Procedure, - TValidate extends boolean, > = TProcedure extends Procedure< any, any, infer UInputSchema, infer UOutputSchema, - infer UHandlerOutput + infer UFuncOutput > ? ( - input: TValidate extends true - ? SchemaInput - : SchemaOutput, + input: SchemaInput | FormData, ) => Promise< - TValidate extends true - ? SchemaOutput - : SchemaInput + SchemaOutput > : never export function createProcedureCaller< TProcedure extends Procedure, - TValidate extends boolean = true, >( - options: CreateProcedureCallerOptions, -): ProcedureCaller { - const internal = options.internal ?? true + options: CreateProcedureCallerOptions, +): ProcedureCaller { const path = options.path ?? [] const procedure = options.procedure - const validate = options.validate ?? true const caller = async (input: unknown): Promise => { - const validInput = (() => { - if (!validate) + const input_ = (() => { + if (!(input instanceof FormData)) { return input + } + + const transformer = new OpenAPIDeserializer({ + schema: procedure.zz$p.contract.zz$cp.InputSchema, + }) + + return transformer.deserializeAsFormData(input) + })() + + const validInput = (() => { const schema = procedure.zz$p.contract.zz$cp.InputSchema - if (!schema) - return input + if (!schema) { + return input_ + } try { - return schema.parse(input) + return schema.parse(input_) } catch (e) { throw new ORPCError({ @@ -92,7 +86,7 @@ export function createProcedureCaller< const middlewares = procedure.zz$p.middlewares ?? [] let currentMidIndex = 0 - let currentContext: Context = options.context + let currentContext: Context = await resolvePromisableValue(options.context) const next: MiddlewareMeta['next'] = async (nextOptions) => { const mid = middlewares[currentMidIndex] @@ -103,17 +97,15 @@ export function createProcedureCaller< return await mid(validInput, currentContext, { path, procedure, - internal, next, output: output => ({ output, context: undefined }), }) } else { return { - output: await await procedure.zz$p.handler(validInput, currentContext, { + output: await await procedure.zz$p.func(validInput, currentContext, { path, procedure, - internal, }), context: currentContext, } @@ -123,11 +115,11 @@ export function createProcedureCaller< const output = (await next({})).output const validOutput = await (async () => { - if (!validate) - return output const schema = procedure.zz$p.contract.zz$cp.OutputSchema - if (!schema) + if (!schema) { return output + } + const result = await schema.safeParseAsync(output) if (result.error) { throw new ORPCError({ @@ -142,5 +134,5 @@ export function createProcedureCaller< return validOutput } - return caller as ProcedureCaller + return caller as ProcedureCaller } diff --git a/packages/server/src/procedure-implementer.test.ts b/packages/server/src/procedure-implementer.test.ts index cb729715e..331b43cde 100644 --- a/packages/server/src/procedure-implementer.test.ts +++ b/packages/server/src/procedure-implementer.test.ts @@ -111,9 +111,9 @@ describe('use middleware', () => { describe('output schema', () => { it('auto infer output schema if output schema is not specified', async () => { - const sr = os.handler(() => ({ a: 1 })) + const sr = os.func(() => ({ a: 1 })) - const result = await sr.zz$p.handler({}, undefined, { + const result = await sr.zz$p.func({}, undefined, { method: 'GET', path: '/', } as any) @@ -129,9 +129,9 @@ describe('output schema', () => { }), }) - const sr = srb1.handler(() => ({ b: 1 })) + const sr = srb1.func(() => ({ b: 1 })) - const result = await sr.zz$p.handler({}, {}, { + const result = await sr.zz$p.func({}, {}, { method: 'GET', path: '/', } as any) @@ -142,7 +142,7 @@ describe('output schema', () => { describe('handler', () => { it('infer types', () => { - const handler = implementer1.handler((input, context, meta) => { + const handler = implementer1.func((input, context, meta) => { expectTypeOf(input).toEqualTypeOf() expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>() expectTypeOf(meta).toEqualTypeOf() @@ -163,7 +163,7 @@ describe('handler', () => { >() expect(isProcedure(handler)).toBe(true) - implementer2.handler((input, context, meta) => { + implementer2.func((input, context, meta) => { expectTypeOf(input).toEqualTypeOf<{ id: string }>() expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>() expectTypeOf(meta).toEqualTypeOf() @@ -174,7 +174,7 @@ describe('handler', () => { }) // @ts-expect-error mismatch output - implementer2.handler(() => {}) + implementer2.func(() => {}) }) it('combine middlewares', () => { @@ -193,7 +193,7 @@ describe('handler', () => { const handler = implementer2 .use(mid1) .use(mid2) - .handler((input, context, meta) => { + .func((input, context, meta) => { expectTypeOf(input).toEqualTypeOf<{ id: string }>() expectTypeOf(context).toEqualTypeOf< { auth: boolean } & { userId: string } diff --git a/packages/server/src/procedure-implementer.ts b/packages/server/src/procedure-implementer.ts index 9282b8e7b..93f093d01 100644 --- a/packages/server/src/procedure-implementer.ts +++ b/packages/server/src/procedure-implementer.ts @@ -8,7 +8,7 @@ import { import { type DecoratedProcedure, decorateProcedure, - type ProcedureHandler, + type ProcedureFunc, } from './procedure' export class ProcedureImplementer< @@ -76,26 +76,26 @@ export class ProcedureImplementer< }) } - handler>( - handler: ProcedureHandler< + func>( + func: ProcedureFunc< TContext, TExtraContext, TInputSchema, TOutputSchema, - UHandlerOutput + UFuncOutput >, ): DecoratedProcedure< TContext, TExtraContext, TInputSchema, TOutputSchema, - UHandlerOutput + UFuncOutput > { return decorateProcedure({ zz$p: { middlewares: this.zz$pi.middlewares, contract: this.zz$pi.contract, - handler, + func, }, }) } diff --git a/packages/server/src/procedure.test.ts b/packages/server/src/procedure.test.ts index ccef90b01..8b1515e07 100644 --- a/packages/server/src/procedure.test.ts +++ b/packages/server/src/procedure.test.ts @@ -18,7 +18,7 @@ it('isProcedure', () => { InputSchema: undefined, OutputSchema: undefined, }), - handler: () => {}, + func: () => {}, }), ), ), @@ -29,7 +29,7 @@ it('isProcedure', () => { InputSchema: undefined, OutputSchema: undefined, }), - handler: () => {}, + func: () => {}, }, }).toSatisfy(isProcedure) @@ -57,7 +57,7 @@ it('isProcedure', () => { describe('route method', () => { it('sets route options correctly', () => { - const p = os.context<{ auth: boolean }>().handler(() => { + const p = os.context<{ auth: boolean }>().func(() => { return 'test' }) @@ -69,11 +69,11 @@ describe('route method', () => { it('preserves existing context and handler', () => { const handler = () => 'test' - const p = os.context<{ auth: boolean }>().handler(handler) + const p = os.context<{ auth: boolean }>().func(handler) const p2 = p.route({ path: '/test' }) - expect(p2.zz$p.handler).toBe(handler) + expect(p2.zz$p.func).toBe(handler) // Context type is preserved through the route method expectTypeOf(p2).toEqualTypeOf< DecoratedProcedure< @@ -90,7 +90,7 @@ describe('route method', () => { const p = os .context<{ auth: boolean }>() .route({ path: '/api', method: 'POST' }) - .handler(() => 'test') + .func(() => 'test') const p2 = p.prefix('/v1') @@ -105,7 +105,7 @@ describe('route method', () => { .context<{ auth: boolean }>() .route({ path: '/test' }) .use(mid) - .handler((input, context) => { + .func((input, context) => { expectTypeOf(context).toEqualTypeOf< { auth: boolean } & { userId: string } >() @@ -120,7 +120,7 @@ describe('route method', () => { const p = os .context<{ auth: boolean }>() .route({ path: '/test1', method: 'GET' }) - .handler(() => 'test') + .func(() => 'test') const p2 = p.route({ path: '/test2', method: 'POST' }) @@ -136,7 +136,7 @@ describe('route method', () => { .input(inputSchema) .output(outputSchema) .route({ path: '/test' }) - .handler((input) => { + .func((input) => { expectTypeOf(input).toEqualTypeOf<{ id: number }>() return 'test' }) @@ -157,7 +157,7 @@ describe('route method', () => { }) it('prefix method', () => { - const p = os.context<{ auth: boolean }>().handler(() => { + const p = os.context<{ auth: boolean }>().func(() => { return 'unnoq' }) @@ -168,7 +168,7 @@ it('prefix method', () => { const p3 = os .context<{ auth: boolean }>() .route({ path: '/test1' }) - .handler(() => { + .func(() => { return 'unnoq' }) @@ -183,7 +183,7 @@ describe('use middleware', () => { .use((_, __, meta) => { return meta.next({ context: { postId: 'string' } }) }) - .handler(() => { + .func(() => { return 'unnoq' }) @@ -251,7 +251,7 @@ describe('use middleware', () => { const mid2 = vi.fn() const mid3 = vi.fn() - const p1 = os.use(mid1).handler(() => 'unnoq') + const p1 = os.use(mid1).func(() => 'unnoq') const p2 = p1.use(mid2).use(mid3) expect(p2.zz$p.middlewares).toEqual([mid3, mid2, mid1]) @@ -260,12 +260,12 @@ describe('use middleware', () => { describe('server action', () => { it('only accept undefined context', () => { - expectTypeOf(os.handler(() => {})).toMatchTypeOf<(...args: any[]) => any>() + expectTypeOf(os.func(() => {})).toMatchTypeOf<(...args: any[]) => any>() expectTypeOf( - os.context<{ auth: boolean } | undefined>().handler(() => {}), + os.context<{ auth: boolean } | undefined>().func(() => {}), ).toMatchTypeOf<(...args: any[]) => any>() expectTypeOf( - os.context<{ auth: boolean }>().handler(() => {}), + os.context<{ auth: boolean }>().func(() => {}), ).not.toMatchTypeOf<(...args: any[]) => any>() }) @@ -273,13 +273,13 @@ describe('server action', () => { const p = os .input(z.object({ id: z.number() })) .output(z.string()) - .handler(() => 'string') + .func(() => 'string') expectTypeOf(p).toMatchTypeOf< (input: { id: number } | FormData) => Promise >() - const p2 = os.input(z.object({ id: z.number() })).handler(() => 12333) + const p2 = os.input(z.object({ id: z.number() })).func(() => 12333) expectTypeOf(p2).toMatchTypeOf< (input: { id: number } | FormData) => Promise @@ -289,7 +289,7 @@ describe('server action', () => { it('works with input', async () => { const p = os .input(z.object({ id: z.number(), date: z.date() })) - .handler(async (input, context) => { + .func(async (input, context) => { expect(context).toBe(undefined) return input }) @@ -303,7 +303,7 @@ describe('server action', () => { it('can deserialize form data', async () => { const p = os .input(z.object({ id: z.number(), nested: z.object({ date: z.date() }) })) - .handler(async input => input) + .func(async input => input) const form = new FormData() form.append('id', '123') diff --git a/packages/server/src/procedure.ts b/packages/server/src/procedure.ts index 36101e8fa..fe54368d5 100644 --- a/packages/server/src/procedure.ts +++ b/packages/server/src/procedure.ts @@ -1,4 +1,5 @@ import type { Promisable } from '@orpc/shared' +import type { ProcedureCaller } from './procedure-caller' import type { Context, MergeContext, Meta } from './types' import { type ContractProcedure, @@ -10,7 +11,6 @@ import { type SchemaInput, type SchemaOutput, } from '@orpc/contract' -import { OpenAPIDeserializer } from '@orpc/transformer' import { decorateMiddleware, type MapInputMiddleware, @@ -23,18 +23,18 @@ export class Procedure< TExtraContext extends Context, TInputSchema extends Schema, TOutputSchema extends Schema, - THandlerOutput extends SchemaOutput, + TFuncOutput extends SchemaOutput, > { constructor( public zz$p: { middlewares?: Middleware[] contract: ContractProcedure - handler: ProcedureHandler< + func: ProcedureFunc< TContext, TExtraContext, TInputSchema, TOutputSchema, - THandlerOutput + TFuncOutput > }, ) {} @@ -45,13 +45,13 @@ export type DecoratedProcedure< TExtraContext extends Context, TInputSchema extends Schema, TOutputSchema extends Schema, - THandlerOutput extends SchemaOutput, + TFuncOutput extends SchemaOutput, > = Procedure< TContext, TExtraContext, TInputSchema, TOutputSchema, - THandlerOutput + TFuncOutput > & { prefix: ( prefix: HTTPPath, @@ -60,7 +60,7 @@ export type DecoratedProcedure< TExtraContext, TInputSchema, TOutputSchema, - THandlerOutput + TFuncOutput > route: ( @@ -70,7 +70,7 @@ export type DecoratedProcedure< TExtraContext, TInputSchema, TOutputSchema, - THandlerOutput + TFuncOutput > use: (< @@ -82,14 +82,14 @@ export type DecoratedProcedure< MergeContext, UExtraContext, SchemaOutput, - SchemaInput + SchemaInput >, ) => DecoratedProcedure< TContext, MergeContext, TInputSchema, TOutputSchema, - THandlerOutput + TFuncOutput >) & (< UExtraContext extends | Partial>> @@ -100,10 +100,10 @@ export type DecoratedProcedure< MergeContext, UExtraContext, UMappedInput, - SchemaInput + SchemaInput >, mapInput: MapInputMiddleware< - SchemaOutput, + SchemaOutput, UMappedInput >, ) => DecoratedProcedure< @@ -111,15 +111,19 @@ export type DecoratedProcedure< MergeContext, TInputSchema, TOutputSchema, - THandlerOutput + TFuncOutput >) } & (undefined extends TContext - ? ( - input: SchemaInput | FormData, - ) => Promise> + ? ProcedureCaller> : unknown) -export interface ProcedureHandler< +export interface ProcedureFunc< TContext extends Context, TExtraContext extends Context, TInputSchema extends Schema, @@ -140,49 +144,30 @@ export function decorateProcedure< TExtraContext extends Context, TInputSchema extends Schema, TOutputSchema extends Schema, - THandlerOutput extends SchemaOutput, + TFuncOutput extends SchemaOutput, >( procedure: Procedure< TContext, TExtraContext, TInputSchema, TOutputSchema, - THandlerOutput + TFuncOutput >, ): DecoratedProcedure< TContext, TExtraContext, TInputSchema, TOutputSchema, - THandlerOutput + TFuncOutput > { if (DECORATED_PROCEDURE_SYMBOL in procedure) { return procedure as any } - const serverAction = async (input: unknown): Promise> => { - const input_ = (() => { - if (!(input instanceof FormData)) - return input - - const transformer = new OpenAPIDeserializer({ - schema: procedure.zz$p.contract.zz$cp.InputSchema, - }) - - return transformer.deserializeAsFormData(input) - })() - - const procedureCaller = createProcedureCaller({ - procedure, - context: undefined as any, - internal: false, - validate: true, - }) - - return await procedureCaller(input_ as any) - } - - return Object.assign(serverAction, { + return Object.assign(createProcedureCaller({ + procedure, + context: undefined as any, + }), { [DECORATED_PROCEDURE_SYMBOL]: true, zz$p: procedure.zz$p, @@ -246,7 +231,7 @@ export function isProcedure(item: unknown): item is WELL_DEFINED_PROCEDURE { && item.zz$p !== null && 'contract' in item.zz$p && isContractProcedure(item.zz$p.contract) - && 'handler' in item.zz$p - && typeof item.zz$p.handler === 'function' + && 'func' in item.zz$p + && typeof item.zz$p.func === 'function' ) } diff --git a/packages/server/src/router-builder.test.ts b/packages/server/src/router-builder.test.ts index 8af486667..64c061f52 100644 --- a/packages/server/src/router-builder.test.ts +++ b/packages/server/src/router-builder.test.ts @@ -6,10 +6,10 @@ import { RouterBuilder } from './router-builder' const builder = new RouterBuilder({}) const ping = os .route({ method: 'GET', path: '/ping', tags: ['ping'] }) - .handler(() => 'ping') + .func(() => 'ping') const pong = os .output(z.object({ id: z.string() })) - .handler(() => ({ id: '123' })) + .func(() => ({ id: '123' })) describe('prefix', () => { it('chainable prefix', () => { @@ -80,7 +80,7 @@ describe('middleware', () => { InputSchema: undefined, OutputSchema: undefined, }), - handler: () => {}, + func: () => {}, }) const decorated = decorateProcedure({ @@ -89,7 +89,7 @@ describe('middleware', () => { InputSchema: undefined, OutputSchema: undefined, }), - handler: () => {}, + func: () => {}, }, }) diff --git a/packages/server/src/router-caller.test.ts b/packages/server/src/router-caller.test.ts index 36ec23aa5..5bca5cab9 100644 --- a/packages/server/src/router-caller.test.ts +++ b/packages/server/src/router-caller.test.ts @@ -2,24 +2,22 @@ import { z } from 'zod' import { createRouterCaller, os } from '.' describe('createRouterCaller', () => { - let internal = false - let context = { auth: true } + const internal = false + const context = { auth: true } const osw = os.context<{ auth?: boolean }>() const ping = osw .input(z.object({ value: z.string().transform(v => Number(v)) })) .output(z.object({ value: z.number().transform(v => v.toString()) })) - .handler((input, context, meta) => { + .func((input, context, meta) => { expect(context).toEqual(context) - expect(meta.internal).toEqual(internal) return input }) - const pong = osw.handler((_, context, meta) => { + const pong = osw.func((_, context, meta) => { expect(context).toEqual(context) - expect(meta.internal).toBe(internal) return { value: true } }) @@ -50,7 +48,6 @@ describe('createRouterCaller', () => { const caller = createRouterCaller({ router, context, - internal, }) expectTypeOf(caller.ping).toMatchTypeOf< @@ -98,52 +95,8 @@ describe('createRouterCaller', () => { ) }) - it('without validate', () => { - internal = true - context = { auth: false } - - const caller = createRouterCaller({ - router, - context, - internal, - validate: false, - }) - - expectTypeOf(caller.ping).toMatchTypeOf< - (input: { value: number }) => Promise<{ - value: number - }> - >() - - expectTypeOf(caller.pong).toMatchTypeOf< - (input: unknown) => Promise<{ - value: boolean - }> - >() - - expectTypeOf(caller.nested.ping).toMatchTypeOf< - (input: { value: number }) => Promise<{ - value: number - }> - >() - - expectTypeOf(caller.nested.pong).toMatchTypeOf< - (input: unknown) => Promise<{ - value: boolean - }> - >() - - expect(caller.ping({ value: 123 })).resolves.toEqual({ value: 123 }) - expect(caller.pong({ value: 123 })).resolves.toEqual({ value: true }) - expect(caller.nested.ping({ value: 123 })).resolves.toEqual({ value: 123 }) - expect(caller.nested.pong({ value: 123 })).resolves.toEqual({ value: true }) - - // @ts-expect-error it's not validate so bellow still works - expect(caller.ping({ value: '123' })).resolves.toEqual({ value: '123' }) - }) - it('path', () => { - const ping = osw.handler((_, __, { path }) => { + const ping = osw.func((_, __, { path }) => { return path }) diff --git a/packages/server/src/router-caller.ts b/packages/server/src/router-caller.ts index 5b97ddab2..98e5493ef 100644 --- a/packages/server/src/router-caller.ts +++ b/packages/server/src/router-caller.ts @@ -1,59 +1,43 @@ -import type {} from '@orpc/contract' +import type { PromisableValue } from '@orpc/shared' import type { Router } from './router' import { isProcedure, type Procedure } from './procedure' import { createProcedureCaller, type ProcedureCaller } from './procedure-caller' export interface CreateRouterCallerOptions< TRouter extends Router, - TValidate extends boolean, > { router: TRouter /** * The context used when calling the procedure. */ - context: TRouter extends Router ? UContext : never + context: PromisableValue< + TRouter extends Router ? UContext : never + > /** * This is helpful for logging and analytics. - */ - basePath?: string[] - - /** - * This flag helpful when you want bypass some logics not necessary to internal server calls. - * - * @default true - */ - internal?: boolean - - /** - * Indicate whether validate input and output. * - * @default true + * @internal */ - validate?: TValidate + basePath?: string[] } export type RouterCaller< TRouter extends Router, - TValidate extends boolean, > = { [K in keyof TRouter]: TRouter[K] extends Procedure - ? ProcedureCaller + ? ProcedureCaller : TRouter[K] extends Router - ? RouterCaller + ? RouterCaller : never } export function createRouterCaller< TRouter extends Router, - TValidate extends boolean = true, >( - options: CreateRouterCallerOptions, -): RouterCaller { - const internal = options.internal ?? true - const validate = options.validate ?? true - + options: CreateRouterCallerOptions, +): RouterCaller { const caller: Record = {} for (const key in options.router) { @@ -65,8 +49,6 @@ export function createRouterCaller< procedure: item, context: options.context as any, path, - internal, - validate, }) } else { @@ -74,11 +56,9 @@ export function createRouterCaller< router: item as any, context: options.context, basePath: path, - internal, - validate, }) } } - return caller as RouterCaller + return caller as RouterCaller } diff --git a/packages/server/src/router-implementer.test.ts b/packages/server/src/router-implementer.test.ts index 3113d4bfb..1c66067d7 100644 --- a/packages/server/src/router-implementer.test.ts +++ b/packages/server/src/router-implementer.test.ts @@ -17,15 +17,15 @@ const cr = oc.router({ const osw = os.context<{ auth: boolean }>().contract(cr) -const p1 = osw.p1.handler(() => { +const p1 = osw.p1.func(() => { return 'unnoq' }) -const p2 = osw.nested.p2.handler(() => { +const p2 = osw.nested.p2.func(() => { return 'unnoq' }) -const p3 = osw.nested2.p3.handler(() => { +const p3 = osw.nested2.p3.func(() => { return 'unnoq' }) @@ -37,7 +37,7 @@ it('required all procedure match', () => { implementer.router({ p1, nested: { - p2: os.contract(cp2).handler(() => ''), + p2: os.contract(cp2).func(() => ''), }, nested2: { p3, @@ -47,7 +47,7 @@ it('required all procedure match', () => { expect(() => { implementer.router({ // @ts-expect-error p1 is mismatch - p1: os.handler(() => {}), + p1: os.func(() => {}), nested: { p2, }, @@ -76,7 +76,7 @@ it('required all procedure match', () => { p1: os .input(z.string()) .output(z.string()) - .handler(() => 'unnoq'), + .func(() => 'unnoq'), nested: { p2, }, diff --git a/packages/server/src/router.test-d.ts b/packages/server/src/router.test-d.ts index 890cbac0f..edb4fd517 100644 --- a/packages/server/src/router.test-d.ts +++ b/packages/server/src/router.test-d.ts @@ -6,11 +6,11 @@ const router = os.router({ ping: os .input(z.object({ ping: z.string().transform(() => 1) })) .output(z.object({ pong: z.number().transform(() => '1') })) - .handler(() => ({ pong: 1 })), + .func(() => ({ pong: 1 })), user: { find: os .input(z.object({ find: z.number().transform(() => '1') })) - .handler(() => ({ user: { id: 1 } })) + .func(() => ({ user: { id: 1 } })) , }, }) diff --git a/packages/server/src/router.test.ts b/packages/server/src/router.test.ts index 35b747cc5..fa14e51be 100644 --- a/packages/server/src/router.test.ts +++ b/packages/server/src/router.test.ts @@ -6,22 +6,22 @@ it('require procedure match context', () => { const osw = os.context<{ auth: boolean, userId: string }>() osw.router({ - ping: osw.context<{ auth: boolean }>().handler(() => { + ping: osw.context<{ auth: boolean }>().func(() => { return { pong: 'ping' } }), // @ts-expect-error userId is not match - ping2: osw.context<{ userId: number }>().handler(() => { + ping2: osw.context<{ userId: number }>().func(() => { return { name: 'unnoq' } }), nested: { - ping: osw.context<{ auth: boolean }>().handler(() => { + ping: osw.context<{ auth: boolean }>().func(() => { return { pong: 'ping' } }), // @ts-expect-error userId is not match - ping2: osw.context<{ userId: number }>().handler(() => { + ping2: osw.context<{ userId: number }>().func(() => { return { name: 'unnoq' } }), }, @@ -31,10 +31,10 @@ it('require procedure match context', () => { it('require match contract', () => { const pingContract = oc.route({ method: 'GET', path: '/ping' }) const pongContract = oc.input(z.string()).output(z.string()) - const ping = os.contract(pingContract).handler(() => { + const ping = os.contract(pingContract).func(() => { return 'ping' }) - const pong = os.contract(pongContract).handler(() => { + const pong = os.contract(pongContract).func(() => { return 'pong' }) @@ -85,7 +85,7 @@ it('require match contract', () => { nested: { ping, // @ts-expect-error nested.pong is mismatch - pong: os.handler(() => 'ping'), + pong: os.func(() => 'ping'), }, } @@ -120,18 +120,18 @@ it('toContractRouter', () => { const osw = os.contract(contract) const router = osw.router({ - p1: osw.p1.handler(() => { + p1: osw.p1.func(() => { return 'unnoq' }), nested: osw.nested.router({ - p2: osw.nested.p2.handler(() => { + p2: osw.nested.p2.func(() => { return 'unnoq' }), }), nested2: { - p3: osw.nested2.p3.handler(() => { + p3: osw.nested2.p3.func(() => { return 'unnoq' }), }, diff --git a/packages/server/src/router.ts b/packages/server/src/router.ts index 9a121193f..c8a54e7bd 100644 --- a/packages/server/src/router.ts +++ b/packages/server/src/router.ts @@ -24,14 +24,14 @@ export type HandledRouter> = { infer UExtraContext, infer UInputSchema, infer UOutputSchema, - infer UHandlerOutput + infer UFuncOutput > ? DecoratedProcedure< UContext, UExtraContext, UInputSchema, UOutputSchema, - UHandlerOutput + UFuncOutput > : TRouter[K] extends Router ? HandledRouter @@ -83,8 +83,8 @@ export type InferRouterInputs> = { } export type InferRouterOutputs> = { - [K in keyof T]: T[K] extends Procedure - ? SchemaOutput + [K in keyof T]: T[K] extends Procedure + ? SchemaOutput : T[K] extends Router ? InferRouterOutputs : never diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index 1b883f375..bc801a1fd 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -9,6 +9,5 @@ export type MergeContext< export interface Meta { path: string[] - internal: boolean procedure: WELL_DEFINED_PROCEDURE } diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index e70b715b2..c0ca5afbb 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,6 +1,7 @@ export * from './json' export * from './object' +export * from './value' export { isPlainObject } from 'is-what' export { guard, mapEntries, mapValues, omit, trim } from 'radash' diff --git a/packages/shared/src/value.test.ts b/packages/shared/src/value.test.ts new file mode 100644 index 000000000..fdd062995 --- /dev/null +++ b/packages/shared/src/value.test.ts @@ -0,0 +1,169 @@ +import { resolvePromisableValue } from './value' + +describe('resolvePromisableValue', () => { + // Test resolving primitive values + describe('primitive Values', () => { + it('should resolve a primitive number', async () => { + const result = await resolvePromisableValue(42) + expect(result).toBe(42) + }) + + it('should resolve a primitive string', async () => { + const result = await resolvePromisableValue('hello') + expect(result).toBe('hello') + }) + + it('should resolve a primitive boolean', async () => { + const result = await resolvePromisableValue(true) + expect(result).toBe(true) + }) + + it('should resolve null', async () => { + const result = await resolvePromisableValue(null) + expect(result).toBe(null) + }) + + it('should resolve undefined', async () => { + const result = await resolvePromisableValue(undefined) + expect(result).toBe(undefined) + }) + }) + + // Test resolving promise-like values + describe('promise-like Values', () => { + it('should resolve a promise with a number', async () => { + const result = await resolvePromisableValue(Promise.resolve(42)) + expect(result).toBe(42) + }) + + it('should resolve a promise with a string', async () => { + const result = await resolvePromisableValue(Promise.resolve('hello')) + expect(result).toBe('hello') + }) + + it('should resolve a promise with an object', async () => { + const testObj = { key: 'value' } + const result = await resolvePromisableValue(Promise.resolve(testObj)) + expect(result).toEqual(testObj) + }) + }) + + // Test resolving function-based values + describe('function-based Values', () => { + it('should resolve a synchronous function returning a number', async () => { + const result = await resolvePromisableValue(() => 42) + expect(result).toBe(42) + }) + + it('should resolve a synchronous function returning a string', async () => { + const result = await resolvePromisableValue(() => 'hello') + expect(result).toBe('hello') + }) + + it('should resolve an async function', async () => { + const result = await resolvePromisableValue(async () => 42) + expect(result).toBe(42) + }) + + it('should resolve an async function returning a promise', async () => { + const result = await resolvePromisableValue(async () => Promise.resolve('hello')) + expect(result).toBe('hello') + }) + }) + + // Test error handling + describe('error Scenarios', () => { + it('should handle a function throwing an error', async () => { + const errorFunc = () => { + throw new Error('Test error') + } + + await expect(resolvePromisableValue(errorFunc)).rejects.toThrow('Test error') + }) + + it('should handle an async function rejecting', async () => { + const rejectFunc = async () => { + throw new Error('Async error') + } + + await expect(resolvePromisableValue(rejectFunc)).rejects.toThrow('Async error') + }) + }) + + // Type inference tests (compile-time checks) + describe('type Inference', () => { + it('should maintain type inference for different value types', async () => { + // These are compile-time checks, they will fail to compile if type inference is incorrect + const num = await resolvePromisableValue(42) + expectTypeOf(num).toEqualTypeOf() + + const str = await resolvePromisableValue(Promise.resolve('hello')) + expectTypeOf(str).toEqualTypeOf() + + const obj = await resolvePromisableValue(() => ({ key: 'value' })) + expectTypeOf(obj).toEqualTypeOf<{ key: string }>() + }) + }) + + describe('promise-like Objects (Non-Native Promises)', () => { + it('should resolve a simple thenable object', async () => { + const thenableObject = { + then(resolve: (value: number) => void) { + resolve(42) + }, + } + + const result = await resolvePromisableValue(thenableObject) + expect(result).toBe(42) + }) + + it('should resolve a complex thenable object', async () => { + const complexThenable = { + then( + resolve: (value: { data: string }) => void, + reject: (error: Error) => void, + ) { + resolve({ data: 'hello world' }) + }, + } + + const result = await resolvePromisableValue(complexThenable) + expect(result).toEqual({ data: 'hello world' }) + }) + + it('should handle a thenable object with delayed resolution', async () => { + const delayedThenable = { + then(resolve: (value: string) => void) { + setTimeout(() => resolve('delayed value'), 10) + }, + } + + const result = await resolvePromisableValue(delayedThenable) + expect(result).toBe('delayed value') + }) + + it('should propagate errors from thenable objects', async () => { + const errorThenable = { + then( + resolve: (value: any) => void, + reject: (error: Error) => void, + ) { + reject(new Error('Thenable error')) + }, + } + + await expect(resolvePromisableValue(errorThenable)).rejects.toThrow('Thenable error') + }) + + it('should work with a nested promise-like object', async () => { + const nestedThenable = { + then(resolve: (value: { nested: { value: number } }) => void) { + resolve({ nested: { value: 123 } }) + }, + } + + const result = await resolvePromisableValue(nestedThenable) + expect(result).toEqual({ nested: { value: 123 } }) + }) + }) +}) diff --git a/packages/shared/src/value.ts b/packages/shared/src/value.ts new file mode 100644 index 000000000..ffc4b74a2 --- /dev/null +++ b/packages/shared/src/value.ts @@ -0,0 +1,14 @@ +import type { Promisable } from 'type-fest' + +export type PromisableValue = T | (() => Promisable) + +export async function resolvePromisableValue>(value: T): +Promise< + Awaited any ? ReturnType : T> +> { + if (typeof value === 'function') { + return value() + } + + return Promise.resolve(value) as any +} diff --git a/playgrounds/contract-openapi/src/router/auth.ts b/playgrounds/contract-openapi/src/router/auth.ts index e55bdd1dd..6063447da 100644 --- a/playgrounds/contract-openapi/src/router/auth.ts +++ b/playgrounds/contract-openapi/src/router/auth.ts @@ -1,6 +1,6 @@ import { authed, pub } from '../orpc' -export const signup = pub.auth.signup.handler(async (input, context, meta) => { +export const signup = pub.auth.signup.func(async (input, context, meta) => { return { id: '28aa6286-48e9-4f23-adea-3486c86acd55', email: input.email, @@ -8,13 +8,13 @@ export const signup = pub.auth.signup.handler(async (input, context, meta) => { } }) -export const signin = pub.auth.signin.handler(async (input, context, meta) => { +export const signin = pub.auth.signin.func(async (input, context, meta) => { return { token: 'token', } }) -export const refresh = authed.auth.refresh.handler( +export const refresh = authed.auth.refresh.func( async (input, context, meta) => { return { token: 'new-token', @@ -22,12 +22,12 @@ export const refresh = authed.auth.refresh.handler( }, ) -export const revoke = authed.auth.revoke.handler( +export const revoke = authed.auth.revoke.func( async (input, context, meta) => { // Do something }, ) -export const me = authed.auth.me.handler(async (input, context, meta) => { +export const me = authed.auth.me.func(async (input, context, meta) => { return context.user }) diff --git a/playgrounds/contract-openapi/src/router/planet.ts b/playgrounds/contract-openapi/src/router/planet.ts index 4c3cea9ba..a0ca1373a 100644 --- a/playgrounds/contract-openapi/src/router/planet.ts +++ b/playgrounds/contract-openapi/src/router/planet.ts @@ -2,13 +2,13 @@ import { ORPCError } from '@orpc/server' import { planets } from '../data/planet' import { authed, pub } from '../orpc' -export const listPlanets = pub.planet.list.handler( +export const listPlanets = pub.planet.list.func( async (input, context, meta) => { return planets }, ) -export const createPlanet = authed.planet.create.handler( +export const createPlanet = authed.planet.create.func( async (input, context, meta) => { const id = planets.length + 1 @@ -26,7 +26,7 @@ export const createPlanet = authed.planet.create.handler( }, ) -export const findPlanet = pub.planet.find.handler( +export const findPlanet = pub.planet.find.func( async (input, context, meta) => { const planet = planets.find(planet => planet.id === input.id) @@ -41,7 +41,7 @@ export const findPlanet = pub.planet.find.handler( }, ) -export const updatePlanet = authed.planet.update.handler( +export const updatePlanet = authed.planet.update.func( async (input, context, meta) => { const planet = planets.find(planet => planet.id === input.id) @@ -60,7 +60,7 @@ export const updatePlanet = authed.planet.update.handler( }, ) -export const updatePlanetImage = authed.planet.updateImage.handler( +export const updatePlanetImage = authed.planet.updateImage.func( async (input, context, meta) => { const planet = planets.find(planet => planet.id === input.id) @@ -77,7 +77,7 @@ export const updatePlanetImage = authed.planet.updateImage.handler( }, ) -export const deletePlanet = authed.planet.delete.handler( +export const deletePlanet = authed.planet.delete.func( async (input, context, meta) => { const planet = planets.find(planet => planet.id === input.id) diff --git a/playgrounds/expressjs/src/router/auth.ts b/playgrounds/expressjs/src/router/auth.ts index 4499f2508..cc1a2d7ca 100644 --- a/playgrounds/expressjs/src/router/auth.ts +++ b/playgrounds/expressjs/src/router/auth.ts @@ -10,7 +10,7 @@ export const signup = pub }) .input(NewUserSchema) .output(UserSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { return { id: '28aa6286-48e9-4f23-adea-3486c86acd55', email: input.email, @@ -26,7 +26,7 @@ export const signin = pub }) .input(CredentialSchema) .output(TokenSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { return { token: 'token', } @@ -39,7 +39,7 @@ export const refresh = authed summary: 'Refresh a token', }) .output(TokenSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { return { token: 'new-token', } @@ -52,7 +52,7 @@ export const revoke = authed summary: 'Revoke a token', }) .input(TokenSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { // Do something }) @@ -63,6 +63,6 @@ export const me = authed summary: 'Get the current user', }) .output(UserSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { return context.user }) diff --git a/playgrounds/expressjs/src/router/planet.ts b/playgrounds/expressjs/src/router/planet.ts index c3a9679f5..de89841a0 100644 --- a/playgrounds/expressjs/src/router/planet.ts +++ b/playgrounds/expressjs/src/router/planet.ts @@ -22,7 +22,7 @@ export const listPlanets = pub }), ) .output(oz.openapi(z.array(PlanetSchema), { examples: [planets] })) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { return planets }) @@ -34,7 +34,7 @@ export const createPlanet = authed }) .input(NewPlanetSchema) .output(PlanetSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { const id = planets.length + 1 const planet = { @@ -62,7 +62,7 @@ export const findPlanet = pub }), ) .output(PlanetSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { const planet = planets.find(planet => planet.id === input.id) if (!planet) { @@ -83,7 +83,7 @@ export const updatePlanet = authed }) .input(UpdatePlanetSchema) .output(PlanetSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { const planet = planets.find(planet => planet.id === input.id) if (!planet) { @@ -113,7 +113,7 @@ export const updatePlanetImage = authed }), ) .output(PlanetSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { const planet = planets.find(planet => planet.id === input.id) if (!planet) { @@ -140,7 +140,7 @@ export const deletePlanet = authed id: z.number().int().min(1), }), ) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { const planet = planets.find(planet => planet.id === input.id) if (!planet) { diff --git a/playgrounds/nextjs/src/app/actions.ts b/playgrounds/nextjs/src/app/actions.ts index 82ffddc03..74cadd557 100644 --- a/playgrounds/nextjs/src/app/actions.ts +++ b/playgrounds/nextjs/src/app/actions.ts @@ -3,10 +3,10 @@ import { os } from '@orpc/server' import { redirect } from 'next/navigation' -export const pong = os.handler(async () => { +export const pong = os.func(async () => { return 'pong' }) -export const visitScalar = os.handler(async () => { +export const visitScalar = os.func(async () => { redirect('/scalar') }) diff --git a/playgrounds/nextjs/src/router/auth.ts b/playgrounds/nextjs/src/router/auth.ts index 4499f2508..cc1a2d7ca 100644 --- a/playgrounds/nextjs/src/router/auth.ts +++ b/playgrounds/nextjs/src/router/auth.ts @@ -10,7 +10,7 @@ export const signup = pub }) .input(NewUserSchema) .output(UserSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { return { id: '28aa6286-48e9-4f23-adea-3486c86acd55', email: input.email, @@ -26,7 +26,7 @@ export const signin = pub }) .input(CredentialSchema) .output(TokenSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { return { token: 'token', } @@ -39,7 +39,7 @@ export const refresh = authed summary: 'Refresh a token', }) .output(TokenSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { return { token: 'new-token', } @@ -52,7 +52,7 @@ export const revoke = authed summary: 'Revoke a token', }) .input(TokenSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { // Do something }) @@ -63,6 +63,6 @@ export const me = authed summary: 'Get the current user', }) .output(UserSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { return context.user }) diff --git a/playgrounds/nextjs/src/router/planet.ts b/playgrounds/nextjs/src/router/planet.ts index c3a9679f5..de89841a0 100644 --- a/playgrounds/nextjs/src/router/planet.ts +++ b/playgrounds/nextjs/src/router/planet.ts @@ -22,7 +22,7 @@ export const listPlanets = pub }), ) .output(oz.openapi(z.array(PlanetSchema), { examples: [planets] })) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { return planets }) @@ -34,7 +34,7 @@ export const createPlanet = authed }) .input(NewPlanetSchema) .output(PlanetSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { const id = planets.length + 1 const planet = { @@ -62,7 +62,7 @@ export const findPlanet = pub }), ) .output(PlanetSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { const planet = planets.find(planet => planet.id === input.id) if (!planet) { @@ -83,7 +83,7 @@ export const updatePlanet = authed }) .input(UpdatePlanetSchema) .output(PlanetSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { const planet = planets.find(planet => planet.id === input.id) if (!planet) { @@ -113,7 +113,7 @@ export const updatePlanetImage = authed }), ) .output(PlanetSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { const planet = planets.find(planet => planet.id === input.id) if (!planet) { @@ -140,7 +140,7 @@ export const deletePlanet = authed id: z.number().int().min(1), }), ) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { const planet = planets.find(planet => planet.id === input.id) if (!planet) { diff --git a/playgrounds/openapi/src/router/auth.ts b/playgrounds/openapi/src/router/auth.ts index 4499f2508..cc1a2d7ca 100644 --- a/playgrounds/openapi/src/router/auth.ts +++ b/playgrounds/openapi/src/router/auth.ts @@ -10,7 +10,7 @@ export const signup = pub }) .input(NewUserSchema) .output(UserSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { return { id: '28aa6286-48e9-4f23-adea-3486c86acd55', email: input.email, @@ -26,7 +26,7 @@ export const signin = pub }) .input(CredentialSchema) .output(TokenSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { return { token: 'token', } @@ -39,7 +39,7 @@ export const refresh = authed summary: 'Refresh a token', }) .output(TokenSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { return { token: 'new-token', } @@ -52,7 +52,7 @@ export const revoke = authed summary: 'Revoke a token', }) .input(TokenSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { // Do something }) @@ -63,6 +63,6 @@ export const me = authed summary: 'Get the current user', }) .output(UserSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { return context.user }) diff --git a/playgrounds/openapi/src/router/planet.ts b/playgrounds/openapi/src/router/planet.ts index c3a9679f5..de89841a0 100644 --- a/playgrounds/openapi/src/router/planet.ts +++ b/playgrounds/openapi/src/router/planet.ts @@ -22,7 +22,7 @@ export const listPlanets = pub }), ) .output(oz.openapi(z.array(PlanetSchema), { examples: [planets] })) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { return planets }) @@ -34,7 +34,7 @@ export const createPlanet = authed }) .input(NewPlanetSchema) .output(PlanetSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { const id = planets.length + 1 const planet = { @@ -62,7 +62,7 @@ export const findPlanet = pub }), ) .output(PlanetSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { const planet = planets.find(planet => planet.id === input.id) if (!planet) { @@ -83,7 +83,7 @@ export const updatePlanet = authed }) .input(UpdatePlanetSchema) .output(PlanetSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { const planet = planets.find(planet => planet.id === input.id) if (!planet) { @@ -113,7 +113,7 @@ export const updatePlanetImage = authed }), ) .output(PlanetSchema) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { const planet = planets.find(planet => planet.id === input.id) if (!planet) { @@ -140,7 +140,7 @@ export const deletePlanet = authed id: z.number().int().min(1), }), ) - .handler(async (input, context, meta) => { + .func(async (input, context, meta) => { const planet = planets.find(planet => planet.id === input.id) if (!planet) {