Skip to content

Commit

Permalink
feat(server)!: rewrite and tests (#52)
Browse files Browse the repository at this point in the history
* wip

* wip

* procedure builder tests

* procedure implementer tests

* wip

* refactor and 100% coverage for procedure caller

* fix middleware types

* tests for middleware with output is typed

* tests for procedure decorated

* router

* typed for router builder

* context restriction for router builder

* typesafe and prevent duplicate on unshift method

* router builder tests partial

* lazy

* lazy decorated

* improve

* sync

* improve middleware types

* improve merging middleware on unshiftMiddleware

* remove lazy method

* improve at context

* router builder

* router caller

* router implementer

* builder

* reindex

* separate middleware decorated

* procedure can be a router

* improve dedupe in unshiftMiddleware

* improve

* tests for procedure works as a router

* safe object callable

* group hidden mechanism

* router caller now works with procedure as router, and more

* fix

* fetch - handle request

* fetch, and some improvement on caller

* rename caller to client, and some improvements

* hidden tests

* rename client

* merge types on client

* react-query sync

* vue-query sync

* sync openapi

* sync next

* sync react

* sync rest

* sync playground

* fix lazy-decorated tests

* sync docs
  • Loading branch information
unnoq authored Dec 20, 2024
1 parent 56e31fa commit 316c163
Show file tree
Hide file tree
Showing 186 changed files with 6,476 additions and 5,811 deletions.
11 changes: 6 additions & 5 deletions apps/content/content/docs/client/react-query.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ description: Simplify React Query usage with minimal integration using ORPC and

```ts twoslash
import { createORPCReactQueryUtils } from '@orpc/react-query';
import { createORPCClient } from '@orpc/client';
import { createORPCFetchClient } from '@orpc/client';
import type { router } from 'examples/server';

// Create an ORPC client
export const client = createORPCClient<typeof router>({
export const client = createORPCFetchClient<typeof router>({
baseURL: 'http://localhost:3000/api',
});

Expand All @@ -35,12 +35,13 @@ orpc.getting.

```tsx twoslash
import { createORPCReactQueryUtils, RouterUtils } from '@orpc/react-query';
import { createORPCClient } from '@orpc/client';
import { createORPCFetchClient } from '@orpc/client';
import { RouterClient } from '@orpc/server';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { router } from 'examples/server';
import * as React from 'react';

const ORPCContext = React.createContext<RouterUtils<typeof router> | undefined>(undefined);
const ORPCContext = React.createContext<RouterUtils<RouterClient<typeof router>> | undefined>(undefined);

export function useORPC() {
const orpc = React.useContext(ORPCContext);
Expand All @@ -54,7 +55,7 @@ export function useORPC() {

export function ORPCProvider({ children }: { children: React.ReactNode }) {
const [client] = React.useState(() =>
createORPCClient<typeof router>({
createORPCFetchClient<typeof router>({
baseURL: 'http://localhost:3000/api',
})
);
Expand Down
7 changes: 4 additions & 3 deletions apps/content/content/docs/client/react.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@ npm i @orpc/client @orpc/react @tanstack/react-query

```tsx twoslash
import { createORPCReact } from '@orpc/react'
import { createORPCClient } from '@orpc/client'
import { createORPCFetchClient } from '@orpc/client'
import { RouterClient } from '@orpc/server'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'
import type { router } from 'examples/server'
import * as React from 'react'

export const { orpc, ORPCContext } = createORPCReact<typeof router /** or contract router */>()
export const { orpc, ORPCContext } = createORPCReact<RouterClient<typeof router /** or contract router */>>()

export function ORPCProvider({ children }: { children: React.ReactNode }) {
const [client] = useState(() => createORPCClient<typeof router /** must match with createORPCReact*/>({
const [client] = useState(() => createORPCFetchClient<typeof router /** must match with createORPCReact*/>({
baseURL: 'http://localhost:3000/api',
}))
const [queryClient] = useState(() => new QueryClient())
Expand Down
4 changes: 2 additions & 2 deletions apps/content/content/docs/client/vanilla.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ npm i @orpc/client
To create a fully typed client, you need either the type of the [router](/docs/server/router) you intend to use or the [contract](/docs/contract/builder).

```ts twoslash
import { createORPCClient, ORPCError } from '@orpc/client'
import { createORPCFetchClient, ORPCError } from '@orpc/client'
import type { router } from 'examples/server'

const client = createORPCClient<typeof router /* or contract router */>({
const client = createORPCFetchClient<typeof router /* or contract router */>({
baseURL: 'http://localhost:3000/api',
// fetch: optional override for the default fetch function
// headers: provide additional headers
Expand Down
4 changes: 2 additions & 2 deletions apps/content/content/docs/client/vue-query.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ description: Simplify Vue Query usage with minimal integration using ORPC and Ta

```ts twoslash
import { createORPCVueQueryUtils } from '@orpc/vue-query';
import { createORPCClient } from '@orpc/client';
import { createORPCFetchClient } from '@orpc/client';
import type { router } from 'examples/server';

// Create an ORPC client
export const client = createORPCClient<typeof router>({
export const client = createORPCFetchClient<typeof router>({
baseURL: 'http://localhost:3000/api',
});

Expand Down
4 changes: 2 additions & 2 deletions apps/content/content/docs/contract-first.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,10 @@ That's it! The contract definition and implementation are now completely separat
Create a fully typed client using just the contract definition:

```ts twoslash
import { createORPCClient, ORPCError } from '@orpc/client'
import { createORPCFetchClient, ORPCError } from '@orpc/client'
import type { contract } from 'examples/contract'

const client = createORPCClient<typeof contract /* or server router */>({
const client = createORPCFetchClient<typeof contract /* or server router */>({
baseURL: 'http://localhost:3000/prefix',
// fetch: optional override for the default fetch function
// headers: provide additional headers
Expand Down
4 changes: 2 additions & 2 deletions apps/content/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,10 @@ Start the server and visit http://localhost:3000/api/getting?name=yourname to se
Use the fully typed client in any environment:

```ts twoslash
import { createORPCClient, ORPCError } from '@orpc/client'
import { createORPCFetchClient, ORPCError } from '@orpc/client'
import type { router } from 'examples/server'

const client = createORPCClient<typeof router /* or contract router */>({
const client = createORPCFetchClient<typeof router /* or contract router */>({
baseURL: 'http://localhost:3000/api',
// fetch: optional override for the default fetch function
// headers: provide additional headers
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: Caller
title: Caller/Client
description: Make your procedures callable in oRPC.
---

Expand All @@ -9,7 +9,7 @@ You can directly call a procedure if its [Global Context](/docs/server/global-co
For security reasons, context cannot be passed when invoking such procedures directly.

```ts twoslash
import { os, createProcedureCaller } from '@orpc/server'
import { os, createProcedureClient } from '@orpc/server'
import { z } from 'zod'

// ❌ Cannot call this procedure directly because undefined is not assignable to 'Context'
Expand All @@ -36,53 +36,53 @@ const output_ = await router.getting({ name: 'World' }) // output is 'Hello, Wor

## Calling Procedures with Context

For context-sensitive calls, use a Procedure Caller.
A Procedure Caller securely provides the required context during invocation.
For context-sensitive calls, use a Procedure Client.
A Procedure Client securely provides the required context during invocation.

```ts twoslash
import { os, createProcedureCaller } from '@orpc/server'
import { os, createProcedureClient } from '@orpc/server'

type Context = { user?: { id: string } }

const getting = os.context<Context>().func(() => 'pong')

const gettingCaller = createProcedureCaller({
const gettingClient = createProcedureClient({
procedure: getting,
context: async () => {
// you can access headers, cookies, etc. here to create context
return { user: { id: 'example' } }
},
})

const output = await gettingCaller() // output is 'pong'
const output = await gettingClient() // output is 'pong'
```

Now, you can provide context when invoking a procedure.
Additionally, you can use `gettingCaller` as a [Server Action](/docs/server/server-action).
Additionally, you can use `gettingClient` as a [Server Action](/docs/server/server-action).

## Calling Routers with Shared Context

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

```ts twoslash
import { os, createRouterCaller } from '@orpc/server'
import { os, createRouterClient } from '@orpc/server'

const router = os.router({
ping: os.func(() => 'pong')
})

const caller = createRouterCaller({
const client = createRouterClient({
router: router,
context: {},
})

const result = await caller.ping() // result is 'pong'
const result = await client.ping() // result is 'pong'
```

## Summary

- **Direct Calls:** Use when no context is required, or the context accepts `undefined`.
- **Procedure Caller:** Use for securely calling a single procedure with a specific context.
- **Router Caller:** Use for securely calling multiple procedures with shared context.
- **Procedure Client:** Use for securely calling a single procedure with a specific context.
- **Router Client:** Use for securely calling multiple procedures with shared context.

oRPC provides flexible and secure ways to invoke procedures tailored to your application needs.
10 changes: 5 additions & 5 deletions apps/content/content/docs/server/context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const router = pub.router({

Middleware context is the context that is created or modified by middleware.
If your procedure only depends on `Middleware Context`, you can
[call it](/docs/server/caller) or use it as a [Server Action](/docs/server/server-action) directly.
[call it](/docs/server/client) or use it as a [Server Action](/docs/server/server-action) directly.

```ts twoslash
import { os, ORPCError } from '@orpc/server'
Expand Down Expand Up @@ -105,7 +105,7 @@ This pattern is useful for server-side applications where dependencies can be in
rather than relying on global mechanisms like `headers` or `cookies` in Next.js.

```ts twoslash
import { os, ORPCError, createProcedureCaller } from '@orpc/server'
import { os, ORPCError, createProcedureClient } from '@orpc/server'
import { handleFetchRequest, createORPCHandler } from '@orpc/server/fetch'
import { createOpenAPIServerlessHandler, createOpenAPIServerHandler } from '@orpc/openapi/fetch'

Expand Down Expand Up @@ -147,8 +147,8 @@ export function fetch(request: Request) {
}

// If you want to call this procedure or use as server action
// you must create another caller with context by using `createProcedureCaller` or `createRouterCaller`
const caller = createProcedureCaller({
// you must create another client with context by using `createProcedureClient` or `createRouterClient`
const client = createProcedureClient({
procedure: router.getting,
context: async () => {
// some logic to create context
Expand All @@ -159,7 +159,7 @@ const caller = createProcedureCaller({
},
})

const output = await caller()
const output = await client()
```

## Summary
Expand Down
4 changes: 2 additions & 2 deletions apps/content/content/docs/server/file-upload.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ To upload files with oRPC from the client, set up an oRPC client
and pass a `File` object directly to the upload endpoint.

```typescript
import { createORPCClient } from '@orpc/client'
import { createORPCFetchClient } from '@orpc/client'

const client = createORPCClient<typeof appRouter>({
const client = createORPCFetchClient<typeof appRouter>({
baseURL: 'http://localhost:3000',
})

Expand Down
2 changes: 1 addition & 1 deletion apps/content/content/docs/server/lazy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Here's how you can set up and use them:
```typescript twoslash
import { os } from '@orpc/server'

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

// Define a router with lazy loading
const router = pub.router({
Expand Down
2 changes: 1 addition & 1 deletion apps/content/content/docs/server/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"file-upload",
"lazy",
"server-action",
"caller",
"client",
"error-handling",
"data-types",
"integrations",
Expand Down
16 changes: 8 additions & 8 deletions apps/content/content/docs/server/server-action.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ oRPC makes it simple to implement server actions, offering a robust and type-saf
Server actions are supported out of the box and are powered by several key features:

- [Middleware](/docs/server/middleware),
- [Procedure Caller](/docs/server/caller)
- [Procedure Client](/docs/server/client)
- [Smart Conversion](/docs/openapi/smart-conversion)
- [Bracket Notation](/docs/openapi/bracket-notation),

## Requirements

To use a procedure as a server action, the procedure must either:

1. Be [directly callable](/docs/server/caller#direct-procedure-calls), or
2. Use [Calling Procedures with Context](/docs/server/caller#calling-procedures-with-context) to create a callable procedure with context.
1. Be [directly callable](/docs/server/client#direct-procedure-calls), or
2. Use [Calling Procedures with Context](/docs/server/client#calling-procedures-with-context) to create a callable procedure with context.

## Usage

Expand Down Expand Up @@ -172,13 +172,13 @@ automatically convert `1992` into a `bigint` and seamlessly parse objects like `

Some procedures cannot be used as server actions directly. This is typically because they
require additional context, such as user information or other runtime data.
In such cases, you can use [createProcedureCaller](/docs/server/caller#calling-procedures-with-context)
or `createSafeAction` and `createFormAction` (built on top of `createProcedureCaller`)
In such cases, you can use [createProcedureClient](/docs/server/client#calling-procedures-with-context)
or `createSafeAction` and `createFormAction` (built on top of `createProcedureClient`)
to provide the required context dynamically, making the procedure callable and usable as a server action.

```ts twoslash
import { createSafeAction, createFormAction } from '@orpc/next'
import { createProcedureCaller, os } from '@orpc/server'
import { createProcedureClient, os } from '@orpc/server'
import { z } from 'zod'

type Context = { user?: { id: string } }
Expand All @@ -191,15 +191,15 @@ const getting = os
// @errors: 2349
getting({ name: 'Unnoq' }) // ❌ cannot call this procedure directly, and cannot be used as a server action

export const caller = createProcedureCaller({ // or createSafeAction or createFormAction
export const client = createProcedureClient({ // or createSafeAction or createFormAction
procedure: getting,
context: async () => {
// you can access headers, cookies, etc. here to create context
return { user: { id: 'example' } }
},
})

caller({ name: 'Unnoq' }) // ✅ can call this procedure directly, and can be used as a server action
client({ name: 'Unnoq' }) // ✅ can call this procedure directly, and can be used as a server action
```

This flexibility ensures you can adapt server actions to scenarios requiring runtime information, enhancing usability across diverse use cases.
11 changes: 6 additions & 5 deletions apps/content/content/home/client.mdx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<Tabs items={['Client', 'React', 'React Query', 'Vue Query', 'CURL']}>
<Tab value="Client">
```ts twoslash
import { createORPCClient, ORPCError } from '@orpc/client'
import { createORPCFetchClient, ORPCError } from '@orpc/client'
import type { router } from 'examples/server'

const client = createORPCClient<typeof router /* or contract router */>({
const client = createORPCFetchClient<typeof router /* or contract router */>({
baseURL: 'http://localhost:3000/api',
// fetch: optional override for the default fetch function
// headers: provide additional headers
Expand Down Expand Up @@ -44,13 +44,14 @@ try {
<Tab value="React">
```tsx twoslash
import { createORPCReact } from '@orpc/react'
import { createORPCClient } from '@orpc/client'
import { createORPCFetchClient } from '@orpc/client'
import { RouterClient } from '@orpc/server'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'
import type { router } from 'examples/server'
import * as React from 'react'

export const { orpc, ORPCContext } = createORPCReact<typeof router /** or contract router */>()
export const { orpc, ORPCContext } = createORPCReact<RouterClient<typeof router /** or contract router */>>()

// ------------------ Example ------------------

Expand Down Expand Up @@ -117,7 +118,7 @@ const queries = orpc.useQueries(o => [
// ------------------ Provider ------------------

export function ORPCProvider({ children }: { children: React.ReactNode }) {
const [client] = useState(() => createORPCClient<typeof router /** must match with createORPCReact*/>({
const [client] = useState(() => createORPCFetchClient<typeof router /** must match with createORPCReact*/>({
baseURL: 'http://localhost:3000/api',
}))
const [queryClient] = useState(() => new QueryClient())
Expand Down
4 changes: 2 additions & 2 deletions apps/content/content/home/landing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
export const getting = os
.use(authMiddleware) // require auth
.use(cache('5m')) // cache the output
.use(canMiddleware, (i) => i.id) // permission check by id
.route({
path: '/getting/{id}' // dynamic params support
method: 'POST' // custom OpenAPI method
Expand All @@ -16,6 +15,7 @@ export const getting = os
avatar: oz.file().type('image/*')
})
}))
.use(canMiddleware, (i) => i.id) // permission check by id
.output(z.string()) // validate output
.func(async (input) => 'Name and Avatar has been updated')
```
Expand All @@ -38,7 +38,7 @@ const text = await getting({
})
```

The [Procedure Caller](/docs/server/caller) feature lets your procedures behave like regular TypeScript functions.
The [Procedure Client](/docs/server/client) feature lets your procedures behave like regular TypeScript functions.

## Expose It Online with a Fully Typed Client

Expand Down
3 changes: 2 additions & 1 deletion apps/content/examples/react-query.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { RouterClient } from '@orpc/server'
import type { router } from 'examples/server'
import { createORPCReactQueryUtils } from '@orpc/react-query'

export const orpc = createORPCReactQueryUtils<typeof router /** or contract router */>('fake-client' as any)
export const orpc = createORPCReactQueryUtils({} as RouterClient<typeof router /** or contract router */>)
5 changes: 2 additions & 3 deletions apps/content/examples/react.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { RouterClient } from '@orpc/server'
import type { router } from 'examples/server'
import { createORPCReact } from '@orpc/react'
// biome-ignore lint/correctness/noUnusedImports: <explanation>

export const { orpc, ORPCContext }
= createORPCReact<typeof router /** or contract router */>()
export const { orpc, ORPCContext } = createORPCReact<RouterClient<typeof router /** or contract router */>>()
Loading

0 comments on commit 316c163

Please sign in to comment.