Skip to content

Commit 23aa4be

Browse files
authored
feat(server)!: redesign callable ability (#78)
* class * wip * createProcedureClient now accept 2 args * createRouterClient new accept 2 args * callable * actionable * docs * call utils * client context * decorated lazy now not callable * fixed * docs: Server Action tab * fix spacing * fix spacing
1 parent 4220427 commit 23aa4be

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+751
-541
lines changed

apps/content/content/docs/client/react-query.mdx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ queryClient.invalidateQueries({ queryKey: orpc.posts.getPost.key({ input: { id:
135135
Infinite queries require a `cursor` in the input field for pagination.
136136

137137
```tsx twoslash
138-
import { os } from '@orpc/server';
138+
import { os, createRouterClient } from '@orpc/server';
139139
import { z } from 'zod';
140140
import { createORPCReactQueryUtils } from '@orpc/react-query';
141141
import { useInfiniteQuery, useSuspenseInfiniteQuery } from '@tanstack/react-query';
@@ -152,7 +152,8 @@ const router = {
152152
},
153153
};
154154

155-
const orpc = createORPCReactQueryUtils<typeof router>('fake-client' as any);
155+
const client = createRouterClient(router); // or any kind of client
156+
const orpc = createORPCReactQueryUtils(client);
156157

157158
export function MyComponent() {
158159
const query = useInfiniteQuery(

apps/content/content/docs/client/vue-query.mdx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ queryClient.invalidateQueries({ queryKey: orpc.posts.getPost.key({ input: { id:
7676
Infinite queries require a `cursor` in the input field for pagination.
7777

7878
```ts twoslash
79-
import { os } from '@orpc/server';
79+
import { os, createRouterClient } from '@orpc/server';
8080
import { z } from 'zod';
8181
import { createORPCVueQueryUtils } from '@orpc/vue-query';
8282
import { useInfiniteQuery } from '@tanstack/vue-query';
@@ -92,7 +92,8 @@ const router = {
9292
},
9393
};
9494

95-
const orpc = createORPCVueQueryUtils<typeof router>('fake-client' as any);
95+
const client = createRouterClient(router); // or any kind of client
96+
const orpc = createORPCVueQueryUtils(client);
9697

9798
const query = useInfiniteQuery(
9899
orpc.user.list.infiniteOptions({

apps/content/content/docs/server/client.mdx

Lines changed: 35 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -3,86 +3,61 @@ title: Caller/Client
33
description: Make your procedures callable in oRPC.
44
---
55

6-
## Direct Procedure Calls
7-
8-
You can directly call a procedure if its [Global Context](/docs/server/global-context) can accept `undefined`.
9-
For security reasons, context cannot be passed when invoking such procedures directly.
6+
## Make procedures callable with `.callable()`
107

118
```ts twoslash
12-
import { os, createProcedureClient } from '@orpc/server'
9+
import { os } from '@orpc/server'
1310
import { z } from 'zod'
1411

15-
// ❌ Cannot call this procedure directly because undefined is not assignable to 'Context'
16-
const e1 = os.context<{ auth: boolean }>().handler(() => 'pong')
17-
// @errors: 2349
18-
e1() // Error: Procedure 'e1' cannot be called directly
19-
20-
// ✅ Can call this procedure directly because undefined is assignable to 'Context'
21-
const e2 = os.context<{ auth: boolean } | undefined>().handler(() => 'pong')
22-
const o2 = await e2() // Ok, output is 'pong'
23-
24-
// ✅ Can call this procedure directly because undefined is assignable to 'Context'
25-
const getting = os.input(z.object({ name: z.string() })).handler(({ input }) => `Hello, ${input.name}!`)
26-
27-
const router = os.router({
28-
getting
29-
})
30-
31-
// call directly
32-
const output = await getting({ name: 'World' }) // output is 'Hello, World!'
33-
// or through router
34-
const output_ = await router.getting({ name: 'World' }) // output is 'Hello, World!'
12+
export const getting = os
13+
.input(z.object({
14+
name: z.string(),
15+
}))
16+
.output(z.string())
17+
.handler(async ({ input }) => {
18+
return `Hello ${input.name}`
19+
})
20+
.callable()
21+
22+
// use it like a regular function
23+
const output = await getting({ name: 'Unnoq' })
3524
```
3625

37-
## Calling Procedures with Context
38-
39-
For context-sensitive calls, use a Procedure Client.
40-
A Procedure Client securely provides the required context during invocation.
26+
## use `call` helper function
4127

4228
```ts twoslash
43-
import { os, createProcedureClient } from '@orpc/server'
44-
45-
type Context = { user?: { id: string } }
46-
47-
const getting = os.context<Context>().handler(() => 'pong')
48-
49-
const gettingClient = createProcedureClient({
50-
procedure: getting,
51-
context: async () => {
52-
// you can access headers, cookies, etc. here to create context
53-
return { user: { id: 'example' } }
54-
},
55-
})
29+
import { call } from '@orpc/server'
30+
import { os } from '@orpc/server'
31+
import { z } from 'zod'
5632

57-
const output = await gettingClient() // output is 'pong'
33+
export const getting = os
34+
.input(z.object({
35+
name: z.string(),
36+
}))
37+
.output(z.string())
38+
.handler(async ({ input }) => {
39+
return `Hello ${input.name}`
40+
})
41+
42+
// call a procedure without creating a client
43+
const output = await call(getting, { name: 'Unnoq' })
5844
```
5945

60-
Now, you can provide context when invoking a procedure.
61-
Additionally, you can use `gettingClient` as a [Server Action](/docs/server/server-action).
62-
63-
## Calling Routers with Shared Context
46+
## Use `createRouterClient` or `createProcedureClient`
6447

6548
To call multiple procedures with shared context, use a `Router Client`.
6649

6750
```ts twoslash
68-
import { os, createRouterClient } from '@orpc/server'
51+
import { os, createRouterClient, createProcedureClient } from '@orpc/server'
6952

7053
const router = os.router({
7154
ping: os.handler(() => 'pong')
7255
})
7356

74-
const client = createRouterClient({
75-
router: router,
76-
context: {},
77-
})
57+
const client = createRouterClient(router)
7858

7959
const result = await client.ping() // result is 'pong'
80-
```
81-
82-
## Summary
83-
84-
- **Direct Calls:** Use when no context is required, or the context accepts `undefined`.
85-
- **Procedure Client:** Use for securely calling a single procedure with a specific context.
86-
- **Router Client:** Use for securely calling multiple procedures with shared context.
8760

88-
oRPC provides flexible and secure ways to invoke procedures tailored to your application needs.
61+
const pingClient = createProcedureClient(router.ping)
62+
const result2 = await pingClient() // result is 'pong'
63+
```

apps/content/content/docs/server/context.mdx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ If your procedure only depends on `Middleware Context`, you can
4545
[call it](/docs/server/client) or use it as a [Server Action](/docs/server/server-action) directly.
4646

4747
```ts twoslash
48-
import { os, ORPCError } from '@orpc/server'
48+
import { os, ORPCError, call } from '@orpc/server'
4949
import { headers } from 'next/headers'
5050

5151
const base = os.use(async ({ context, path, next }, input) => {
@@ -80,7 +80,7 @@ export const router = base.router({
8080
})
8181

8282
// You can call this procedure directly without manually providing context
83-
const output = await router.getUser()
83+
const output = await call(router.getUser, null)
8484

8585
import { ORPCHandler } from '@orpc/server/fetch'
8686
import { OpenAPIServerlessHandler, OpenAPIServerHandler } from '@orpc/openapi/fetch'
@@ -144,8 +144,7 @@ export async function fetch(request: Request) {
144144

145145
// If you want to call this procedure or use as server action
146146
// you must create another client with context by using `createProcedureClient` or `createRouterClient`
147-
const client = createProcedureClient({
148-
procedure: router.getUser,
147+
const client = createProcedureClient(router.getUser, {
149148
context: async () => {
150149
// some logic to create context
151150
return {

apps/content/content/docs/server/error-handling.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const createPost = os
3131
// throw errors.ANY_CODE()
3232
})
3333

34-
const client = createProcedureClient({ procedure: createPost }) // or any kind of client
34+
const client = createProcedureClient(createPost) // or any kind of client
3535

3636
const [data, error, isDefined] = await safe(client({ title: 'title' }))
3737

apps/content/content/docs/server/lazy.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Lazy routers make your application modular and efficient without sacrificing eas
1414
Here's how you can set up and use them:
1515

1616
```typescript twoslash
17-
import { os } from '@orpc/server'
17+
import { os, call } from '@orpc/server'
1818

1919
const pub = os.context<{ user?: { id: string } } | undefined>()
2020

@@ -26,7 +26,7 @@ const router = pub.router({
2626
})
2727

2828
// Use the lazy-loaded router as if it were a regular one
29-
const output = await router.lazy.getUser({ id: '123' })
29+
const output = await call(router.lazy.getUser, { id: '123' })
3030
```
3131

3232
### Key Points:

apps/content/content/docs/server/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"contract",
88
"file-upload",
99
"lazy",
10+
"server-action",
1011
"client",
1112
"error-handling",
1213
"data-types",
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
---
2+
title: Server Actions
3+
description: Leverage oRPC for type-safe and powerful server actions
4+
---
5+
6+
Server Actions in oRPC allow you to define type-safe server-side procedures that can be seamlessly invoked from client applications.
7+
To enable a procedure as a server action, you need to call the `.actionable()` method.
8+
9+
## Basic Usage
10+
11+
To make a procedure compatible with server actions, use `.actionable()` as shown below:
12+
13+
```ts twoslash
14+
'use server'
15+
16+
import { os } from '@orpc/server'
17+
import { z } from 'zod'
18+
19+
export const getting = os
20+
.input(z.object({
21+
name: z.string(),
22+
}))
23+
.output(z.string())
24+
.handler(async ({ input }) => {
25+
return `Hello ${input.name}`
26+
})
27+
.actionable()
28+
29+
// from client just call it as a function
30+
const onClick = async () => {
31+
const result = await getting({ name: 'Unnoq' })
32+
alert(result)
33+
}
34+
```
35+
36+
## Passing Context via `.actionable()`
37+
38+
When calling `.actionable()`, you can pass a context function that provides additional information for the procedure:
39+
40+
```ts twoslash
41+
'use server'
42+
43+
import { os } from '@orpc/server'
44+
import { z } from 'zod'
45+
46+
const pub = os.context<{ db: string } | undefined>()
47+
48+
export const getting = pub
49+
.input(z.object({
50+
name: z.string(),
51+
}))
52+
.output(z.string())
53+
.handler(async ({ input, context }) => {
54+
// ^ context is fully typed
55+
return `Hello ${input.name}`
56+
})
57+
.actionable({
58+
context: async () => { // or just pass context directly
59+
return { db: 'postgres' }
60+
},
61+
})
62+
```
63+
64+
## Using Middleware to Inject Context
65+
66+
Middleware can be used to inject context dynamically before the procedure is executed:
67+
68+
```ts twoslash
69+
'use server'
70+
71+
import { ORPCError, os } from '@orpc/server'
72+
import { headers } from 'next/headers'
73+
import { z } from 'zod'
74+
75+
const authed = os.use(async ({ next }) => {
76+
const headersList = await headers()
77+
const user = headersList.get('Authorization') ? { id: 'example' } : undefined
78+
79+
if (!user) {
80+
throw new ORPCError({ code: 'UNAUTHORIZED' })
81+
}
82+
83+
return next({
84+
context: {
85+
user,
86+
},
87+
})
88+
})
89+
90+
export const getting = authed
91+
.input(z.object({
92+
name: z.string(),
93+
}))
94+
.output(z.string())
95+
.handler(async ({ input, context }) => {
96+
// ^ context is fully typed
97+
return `Hello ${input.name}`
98+
})
99+
.actionable()
100+
```

apps/content/content/home/landing.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export const updateUser = os
3030
// or throw
3131
throw errors.NOT_FOUND({ data: { why: 'some reason' } })
3232
})
33+
.callable() // make this work like a regular function
34+
.actionable() // like .callable but compatible with server action
3335
```
3436

3537
> Only the `.handler` method is required. All other chain methods are optional.

apps/content/content/home/server.mdx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,15 @@
4242
}
4343
```
4444
</Tab>
45+
46+
<Tab value="Server Action">
47+
```json doc-gen:file
48+
{
49+
"file": "examples/server-action.ts",
50+
"codeblock": {
51+
"meta": "twoslash"
52+
}
53+
}
54+
```
55+
</Tab>
4556
</Tabs>

0 commit comments

Comments
 (0)