From d7b5662b354d68f6fe8bd996027cd0fbafab7cde Mon Sep 17 00:00:00 2001 From: unnoq Date: Sun, 29 Dec 2024 20:28:54 +0700 Subject: [PATCH] feat(client)!: `client context` mechanism and rewrite (#62) * improve & tests * fixed * sync @orpc/react * sync @orpc/react-query * sync @orpc/vue-query * fix global fetch problem * type fixed * lint fixed * fixes * rename createClient -> createORPCClient * docs * dynamic link * improve cast type * client context + dynamic link docs * fix docs * improve docs * improve docs * improve docs --- .../content/docs/client/react-query.mdx | 33 ++- apps/content/content/docs/client/react.mdx | 17 +- apps/content/content/docs/client/vanilla.mdx | 68 ++++- .../content/content/docs/client/vue-query.mdx | 13 +- apps/content/content/docs/contract-first.mdx | 9 +- apps/content/content/docs/index.mdx | 9 +- .../content/docs/server/file-upload.mdx | 11 +- apps/content/content/home/client.mdx | 26 +- apps/content/examples/react-query.ts | 2 +- apps/content/examples/react.ts | 2 +- apps/content/examples/vue-query.ts | 2 +- eslint.config.js | 1 + packages/client/package.json | 8 +- packages/client/src/adapters/fetch/index.ts | 2 + .../src/adapters/fetch/orpc-link.test.ts | 245 ++++++++++++++++++ .../client/src/adapters/fetch/orpc-link.ts | 63 +++++ packages/client/src/adapters/fetch/types.ts | 6 + ...etch-client.test-d.ts => client.test-d.ts} | 28 +- packages/client/src/client.test.ts | 48 ++++ packages/client/src/client.ts | 36 +++ packages/client/src/dynamic-link.test-d.ts | 21 ++ packages/client/src/dynamic-link.test.ts | 26 ++ packages/client/src/dynamic-link.ts | 27 ++ packages/client/src/index.ts | 8 +- .../src/procedure-fetch-client.test-d.ts | 13 - .../client/src/procedure-fetch-client.test.ts | 85 ------ packages/client/src/procedure-fetch-client.ts | 87 ------- .../client/src/router-fetch-client.test.ts | 71 ----- packages/client/src/router-fetch-client.ts | 32 --- packages/client/src/types.ts | 5 + packages/client/tests/helpers.ts | 13 +- packages/client/tsconfig.json | 2 +- packages/next/src/action-safe.ts | 5 +- packages/next/src/client/action-hooks.ts | 2 +- packages/next/src/client/action-safe-hooks.ts | 2 +- packages/react-query/src/types.ts | 14 +- .../react-query/src/utils-procedure.test-d.ts | 101 +++++++- .../react-query/src/utils-procedure.test.ts | 65 ++++- packages/react-query/src/utils-procedure.ts | 30 +-- .../react-query/src/utils-router.test-d.ts | 22 +- packages/react-query/src/utils-router.ts | 10 +- packages/react-query/tests/e2e.test-d.ts | 52 ++++ packages/react-query/tests/helpers.tsx | 13 +- packages/react/src/procedure-utils.ts | 2 +- packages/react/src/react-context.ts | 8 +- packages/react/src/react-hooks.ts | 10 +- packages/react/src/react-utils.ts | 10 +- packages/react/src/react.tsx | 4 +- .../react/src/use-queries/builder.test.ts | 2 +- packages/react/src/use-queries/builder.ts | 2 +- packages/react/src/use-queries/builders.ts | 10 +- packages/react/src/use-queries/hook.ts | 6 +- packages/react/tests/orpc.tsx | 16 +- packages/server/src/lazy-decorated.test-d.ts | 10 +- packages/server/src/lazy-decorated.ts | 2 +- .../server/src/procedure-client.test-d.ts | 46 +++- packages/server/src/procedure-client.ts | 15 +- packages/server/src/procedure-decorated.ts | 2 +- packages/server/src/router-client.test-d.ts | 18 +- packages/server/src/router-client.ts | 14 +- packages/vue-query/src/types.ts | 9 +- .../vue-query/src/utils-procedure.test-d.ts | 119 ++++++++- .../vue-query/src/utils-procedure.test.ts | 67 ++++- packages/vue-query/src/utils-procedure.ts | 30 +-- packages/vue-query/src/utils-router.test-d.ts | 22 +- packages/vue-query/src/utils-router.ts | 10 +- packages/vue-query/tests/e2e.test-d.ts | 79 ++++++ packages/vue-query/tests/helpers.ts | 13 +- .../contract-openapi/src/playground-client.ts | 9 +- .../contract-openapi/src/playground-react.ts | 2 +- .../expressjs/src/playground-client.ts | 9 +- playgrounds/expressjs/src/playground-react.ts | 2 +- playgrounds/nextjs/src/lib/orpc.ts | 9 +- playgrounds/nuxt/lib/orpc.ts | 9 +- playgrounds/openapi/src/playground-client.ts | 9 +- playgrounds/openapi/src/playground-react.ts | 2 +- 76 files changed, 1367 insertions(+), 545 deletions(-) create mode 100644 packages/client/src/adapters/fetch/index.ts create mode 100644 packages/client/src/adapters/fetch/orpc-link.test.ts create mode 100644 packages/client/src/adapters/fetch/orpc-link.ts create mode 100644 packages/client/src/adapters/fetch/types.ts rename packages/client/src/{router-fetch-client.test-d.ts => client.test-d.ts} (61%) create mode 100644 packages/client/src/client.test.ts create mode 100644 packages/client/src/client.ts create mode 100644 packages/client/src/dynamic-link.test-d.ts create mode 100644 packages/client/src/dynamic-link.test.ts create mode 100644 packages/client/src/dynamic-link.ts delete mode 100644 packages/client/src/procedure-fetch-client.test-d.ts delete mode 100644 packages/client/src/procedure-fetch-client.test.ts delete mode 100644 packages/client/src/procedure-fetch-client.ts delete mode 100644 packages/client/src/router-fetch-client.test.ts delete mode 100644 packages/client/src/router-fetch-client.ts create mode 100644 packages/client/src/types.ts diff --git a/apps/content/content/docs/client/react-query.mdx b/apps/content/content/docs/client/react-query.mdx index 354b4c48..38f29aba 100644 --- a/apps/content/content/docs/client/react-query.mdx +++ b/apps/content/content/docs/client/react-query.mdx @@ -15,13 +15,17 @@ description: Simplify React Query usage with minimal integration using ORPC and ```ts twoslash import { createORPCReactQueryUtils } from '@orpc/react-query'; -import { createORPCFetchClient } from '@orpc/client'; +import { createORPCClient } from '@orpc/client'; +import { ORPCLink } from '@orpc/client/fetch'; import type { router } from 'examples/server'; -// Create an ORPC client -export const client = createORPCFetchClient({ - baseURL: 'http://localhost:3000/api', -}); +const orpcLink = new ORPCLink({ + url: 'http://localhost:3000/api', + // fetch: optional override for the default fetch function + // headers: provide additional headers +}) + +const client = createORPCClient(orpcLink) // Create React Query utilities for ORPC export const orpc = createORPCReactQueryUtils(client); @@ -35,13 +39,14 @@ orpc.getting. ```tsx twoslash import { createORPCReactQueryUtils, RouterUtils } from '@orpc/react-query'; -import { createORPCFetchClient } from '@orpc/client'; +import { createORPCClient } from '@orpc/client'; +import { ORPCLink } from '@orpc/client/fetch'; 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> | undefined>(undefined); +const ORPCContext = React.createContext> | undefined>(undefined); export function useORPC() { const orpc = React.useContext(ORPCContext); @@ -54,11 +59,15 @@ export function useORPC() { } export function ORPCProvider({ children }: { children: React.ReactNode }) { - const [client] = React.useState(() => - createORPCFetchClient({ - baseURL: 'http://localhost:3000/api', - }) - ); + const [client] = React.useState(() => { + const orpcLink = new ORPCLink({ + url: 'http://localhost:3000/api', + // fetch: optional override for the default fetch function + // headers: provide additional headers + }); + + return createORPCClient(orpcLink); + }); const [queryClient] = React.useState(() => new QueryClient()); const orpc = React.useMemo(() => createORPCReactQueryUtils(client), [client]); diff --git a/apps/content/content/docs/client/react.mdx b/apps/content/content/docs/client/react.mdx index ba0eea67..53e59439 100644 --- a/apps/content/content/docs/client/react.mdx +++ b/apps/content/content/docs/client/react.mdx @@ -13,19 +13,26 @@ npm i @orpc/client @orpc/react @tanstack/react-query ```tsx twoslash import { createORPCReact } from '@orpc/react' -import { createORPCFetchClient } from '@orpc/client' +import { createORPCClient } from '@orpc/client' +import { ORPCLink } from '@orpc/client/fetch' 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>() +export const { orpc, ORPCContext } = createORPCReact>() export function ORPCProvider({ children }: { children: React.ReactNode }) { - const [client] = useState(() => createORPCFetchClient({ - baseURL: 'http://localhost:3000/api', - })) + const [client] = useState(() => { + const orpcLink = new ORPCLink({ + url: 'http://localhost:3000/api', + // fetch: optional override for the default fetch function + // headers: provide additional headers + }) + + return createORPCClient(orpcLink) + }) const [queryClient] = useState(() => new QueryClient()) return ( diff --git a/apps/content/content/docs/client/vanilla.mdx b/apps/content/content/docs/client/vanilla.mdx index 68feac97..cdcda488 100644 --- a/apps/content/content/docs/client/vanilla.mdx +++ b/apps/content/content/docs/client/vanilla.mdx @@ -14,14 +14,17 @@ 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 { createORPCFetchClient, ORPCError } from '@orpc/client' +import { createORPCClient, ORPCError } from '@orpc/client' +import { ORPCLink } from '@orpc/client/fetch' import type { router } from 'examples/server' -const client = createORPCFetchClient({ - baseURL: 'http://localhost:3000/api', +const orpcLink = new ORPCLink({ + url: 'http://localhost:3000/api', // fetch: optional override for the default fetch function // headers: provide additional headers }) + +const client = createORPCClient(orpcLink) // File upload out of the box const output = await client.post.create({ @@ -34,3 +37,62 @@ const output = await client.post.create({ client.post. // ^| ``` + +## Client Context + +The `Client Context` feature allows you to pass additional contextual information (like caching policies) to your client calls. + +```ts twoslash +import type { router } from 'examples/server' +import { createORPCClient, ORPCError } from '@orpc/client' +import { ORPCLink } from '@orpc/client/fetch' + +type ClientContext = { cache?: RequestCache } | undefined +// if context is not undefinable, it will require you pass context in every call + +const orpcLink = new ORPCLink({ + url: 'http://localhost:3000/api', + // headers: provide additional headers + fetch: (input, init, context) => globalThis.fetch(input, { + ...init, + cache: context?.cache, + }), +}) + +const client = createORPCClient(orpcLink) + +client.getting({ name: 'unnoq' }, { context: { cache: 'force-cache' } }) +``` + +> **Note**: This works seamlessly with [Vue Query](/docs/client/vue-query) and [React Query](/docs/client/react-query). + +## Dynamic Link + +With the **Dynamic Link** mechanism, you can define custom logic to dynamically choose between different links based on the request's context, path, or input. + +```ts twoslash +import type { router } from 'examples/server' +import { createORPCClient, DynamicLink, ORPCError } from '@orpc/client' +import { ORPCLink } from '@orpc/client/fetch' + +const orpcLink1 = new ORPCLink({ + url: 'http://localhost:3000/api', + // headers: provide additional headers +}) + +const orpcLink2 = new ORPCLink({ + url: 'http://localhost:8000/api', + // headers: provide additional headers +}) + +const dynamicLink = new DynamicLink((path, input, options) => { // can be async + // const clientContext = options.context + if (path.includes('post')) { + return orpcLink1 + } + + return orpcLink2 +}) + +const client = createORPCClient(dynamicLink) +``` \ No newline at end of file diff --git a/apps/content/content/docs/client/vue-query.mdx b/apps/content/content/docs/client/vue-query.mdx index 88f55535..74de6326 100644 --- a/apps/content/content/docs/client/vue-query.mdx +++ b/apps/content/content/docs/client/vue-query.mdx @@ -13,14 +13,19 @@ description: Simplify Vue Query usage with minimal integration using ORPC and Ta ```ts twoslash import { createORPCVueQueryUtils } from '@orpc/vue-query'; -import { createORPCFetchClient } from '@orpc/client'; +import { createORPCClient } from '@orpc/client'; +import { ORPCLink } from '@orpc/client/fetch'; import type { router } from 'examples/server'; -// Create an ORPC client -export const client = createORPCFetchClient({ - baseURL: 'http://localhost:3000/api', +const orpcLink = new ORPCLink({ + url: 'http://localhost:3000/api', + // fetch: optional override for the default fetch function + // headers: provide additional headers }); +// Create an ORPC client +export const client = createORPCClient(orpcLink); + // Create Vue Query utilities for ORPC export const orpc = createORPCVueQueryUtils(client); diff --git a/apps/content/content/docs/contract-first.mdx b/apps/content/content/docs/contract-first.mdx index adbd5b41..da1a670c 100644 --- a/apps/content/content/docs/contract-first.mdx +++ b/apps/content/content/docs/contract-first.mdx @@ -129,14 +129,17 @@ 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 { createORPCFetchClient, ORPCError } from '@orpc/client' +import { createORPCClient, ORPCError } from '@orpc/client' +import { ORPCLink } from '@orpc/client/fetch' import type { contract } from 'examples/contract' -const client = createORPCFetchClient({ - baseURL: 'http://localhost:3000/prefix', +const orpcLink = new ORPCLink({ + url: 'http://localhost:3000/prefix', // fetch: optional override for the default fetch function // headers: provide additional headers }) + +const client = createORPCClient(orpcLink) // File upload out of the box const output = await client.post.create({ diff --git a/apps/content/content/docs/index.mdx b/apps/content/content/docs/index.mdx index b72884f0..69985591 100644 --- a/apps/content/content/docs/index.mdx +++ b/apps/content/content/docs/index.mdx @@ -187,14 +187,17 @@ 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 { createORPCFetchClient, ORPCError } from '@orpc/client' +import { createORPCClient, ORPCError } from '@orpc/client' +import { ORPCLink } from '@orpc/client/fetch' import type { router } from 'examples/server' -const client = createORPCFetchClient({ - baseURL: 'http://localhost:3000/api', +const orpcLink = new ORPCLink({ + url: 'http://localhost:3000/api', // fetch: optional override for the default fetch function // headers: provide additional headers }) + +const client = createORPCClient(orpcLink) // File upload out of the box const output = await client.post.create({ diff --git a/apps/content/content/docs/server/file-upload.mdx b/apps/content/content/docs/server/file-upload.mdx index 31b62b6e..e5e63b9d 100644 --- a/apps/content/content/docs/server/file-upload.mdx +++ b/apps/content/content/docs/server/file-upload.mdx @@ -63,12 +63,17 @@ 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 { createORPCFetchClient } from '@orpc/client' +import { createORPCClient } from '@orpc/client' +import { ORPCLink } from '@orpc/client/fetch' -const client = createORPCFetchClient({ - baseURL: 'http://localhost:3000', +const orpcLink = new ORPCLink({ + url: 'http://localhost:3000/api', + // fetch: optional override for the default fetch function + // headers: provide additional headers }) +const client = createORPCClient(orpcLink) + // Example: Upload a file from an HTML file input const fileInput = document.getElementById('file-input') as HTMLInputElement fileInput.onchange = async () => { diff --git a/apps/content/content/home/client.mdx b/apps/content/content/home/client.mdx index 46271470..d45b9653 100644 --- a/apps/content/content/home/client.mdx +++ b/apps/content/content/home/client.mdx @@ -1,14 +1,17 @@ ```ts twoslash -import { createORPCFetchClient, ORPCError } from '@orpc/client' +import { createORPCClient, ORPCError } from '@orpc/client' +import { ORPCLink } from '@orpc/client/fetch' import type { router } from 'examples/server' -const client = createORPCFetchClient({ - baseURL: 'http://localhost:3000/api', +const orpcLink = new ORPCLink({ + url: 'http://localhost:3000/api', // fetch: optional override for the default fetch function // headers: provide additional headers }) + +const client = createORPCClient(orpcLink) // File upload out of the box const output = await client.post.create({ @@ -44,14 +47,15 @@ try { ```tsx twoslash import { createORPCReact } from '@orpc/react' -import { createORPCFetchClient } from '@orpc/client' +import { createORPCClient } from '@orpc/client' +import { ORPCLink } from '@orpc/client/fetch' 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>() +export const { orpc, ORPCContext } = createORPCReact>() // ------------------ Example ------------------ @@ -118,9 +122,15 @@ const queries = orpc.useQueries(o => [ // ------------------ Provider ------------------ export function ORPCProvider({ children }: { children: React.ReactNode }) { - const [client] = useState(() => createORPCFetchClient({ - baseURL: 'http://localhost:3000/api', - })) + const [client] = useState(() => { + const orpcLink = new ORPCLink({ + url: 'http://localhost:3000/api', + // fetch: optional override for the default fetch function + // headers: provide additional headers + }) + + return createORPCClient(orpcLink) + }) const [queryClient] = useState(() => new QueryClient()) return ( diff --git a/apps/content/examples/react-query.ts b/apps/content/examples/react-query.ts index 74ac0a88..10960515 100644 --- a/apps/content/examples/react-query.ts +++ b/apps/content/examples/react-query.ts @@ -2,4 +2,4 @@ import type { RouterClient } from '@orpc/server' import type { router } from 'examples/server' import { createORPCReactQueryUtils } from '@orpc/react-query' -export const orpc = createORPCReactQueryUtils({} as RouterClient) +export const orpc = createORPCReactQueryUtils({} as RouterClient) diff --git a/apps/content/examples/react.ts b/apps/content/examples/react.ts index 516262c7..bfe1604e 100644 --- a/apps/content/examples/react.ts +++ b/apps/content/examples/react.ts @@ -2,4 +2,4 @@ import type { RouterClient } from '@orpc/server' import type { router } from 'examples/server' import { createORPCReact } from '@orpc/react' -export const { orpc, ORPCContext } = createORPCReact>() +export const { orpc, ORPCContext } = createORPCReact>() diff --git a/apps/content/examples/vue-query.ts b/apps/content/examples/vue-query.ts index 29a0fcb4..42cf7588 100644 --- a/apps/content/examples/vue-query.ts +++ b/apps/content/examples/vue-query.ts @@ -2,4 +2,4 @@ import type { RouterClient } from '@orpc/server' import type { router } from 'examples/server' import { createORPCVueQueryUtils } from '@orpc/vue-query' -export const orpc = createORPCVueQueryUtils({} as RouterClient) +export const orpc = createORPCVueQueryUtils({} as RouterClient) diff --git a/eslint.config.js b/eslint.config.js index 246ad594..4e67c5bf 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -8,6 +8,7 @@ export default antfu({ 'ts/consistent-type-definitions': 'off', 'react-refresh/only-export-components': 'off', 'react/prefer-destructuring-assignment': 'off', + 'react/no-context-provider': 'off', }, }, { files: ['**/*.test.ts', '**/*.test.tsx', '**/*.test-d.ts', '**/*.test-d.tsx', 'apps/content/examples/**', 'playgrounds/**'], diff --git a/packages/client/package.json b/packages/client/package.json index 0b5b9408..07f3e021 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -20,6 +20,11 @@ "import": "./dist/index.js", "default": "./dist/index.js" }, + "./fetch": { + "types": "./dist/src/adapters/fetch/index.d.ts", + "import": "./dist/fetch.js", + "default": "./dist/fetch.js" + }, "./🔒/*": { "types": "./dist/src/*.d.ts" } @@ -27,6 +32,7 @@ }, "exports": { ".": "./src/index.ts", + "./fetch": "./src/adapters/fetch/index.ts", "./🔒/*": { "types": "./src/*.ts" } @@ -37,7 +43,7 @@ "dist" ], "scripts": { - "build": "tsup --clean --sourcemap --entry.index=src/index.ts --format=esm --onSuccess='tsc -b --noCheck'", + "build": "tsup --clean --sourcemap --entry.index=src/index.ts --entry.fetch=src/adapters/fetch/index.ts --format=esm --onSuccess='tsc -b --noCheck'", "build:watch": "pnpm run build --watch", "type:check": "tsc -b" }, diff --git a/packages/client/src/adapters/fetch/index.ts b/packages/client/src/adapters/fetch/index.ts new file mode 100644 index 00000000..c01f3ca4 --- /dev/null +++ b/packages/client/src/adapters/fetch/index.ts @@ -0,0 +1,2 @@ +export * from './orpc-link' +export * from './types' diff --git a/packages/client/src/adapters/fetch/orpc-link.test.ts b/packages/client/src/adapters/fetch/orpc-link.test.ts new file mode 100644 index 00000000..3f53e32e --- /dev/null +++ b/packages/client/src/adapters/fetch/orpc-link.test.ts @@ -0,0 +1,245 @@ +import { ORPC_HANDLER_HEADER, ORPC_HANDLER_VALUE } from '@orpc/shared' +import { ORPCError } from '@orpc/shared/error' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { ORPCLink } from './orpc-link' + +describe('oRPCLink', () => { + // Mock setup + const mockFetch = vi.fn() + const mockHeaders = vi.fn() + const mockPayloadCodec = { + encode: vi.fn(), + decode: vi.fn(), + } + + beforeEach(() => { + vi.clearAllMocks() + // Default mock implementations + mockPayloadCodec.encode.mockReturnValue({ headers: {}, body: 'encoded-body' }) + mockPayloadCodec.decode.mockReturnValue({ data: 'decoded-data' }) + }) + + // Test basic successful call + it('should make a successful call with correct parameters', async () => { + const link = new ORPCLink({ + url: 'http://api.example.com', + fetch: mockFetch, + payloadCodec: mockPayloadCodec as any, + }) + + const mockResponseData = { id: 1, name: 'Test User' } + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + }) + mockPayloadCodec.decode.mockResolvedValueOnce(mockResponseData) + + const result = await link.call(['users', 'get'], { id: 1 }, { context: { auth: 'token' } }) + + // Verify fetch was called with correct parameters + expect(mockFetch).toHaveBeenCalledWith( + 'http://api.example.com/users/get', + { + method: 'POST', + headers: expect.any(Headers), + body: 'encoded-body', + signal: undefined, + }, + { auth: 'token' }, + ) + + // Verify headers + const headers = mockFetch.mock.calls[0]![1].headers + expect(headers.get(ORPC_HANDLER_HEADER)).toBe(ORPC_HANDLER_VALUE) + + // Verify payload codec usage + expect(mockPayloadCodec.encode).toHaveBeenCalledWith({ id: 1 }) + expect(mockPayloadCodec.decode).toHaveBeenCalledWith(expect.objectContaining({ + ok: true, + status: 200, + })) + + // Verify the final result matches the decoded data + expect(result).toEqual(mockResponseData) + }) + + // Test custom headers + const headers = [ + () => ({ 'Authorization': 'Bearer token', 'Custom-Header': 'custom-value' }), + async () => ({ 'Authorization': 'Bearer token', 'Custom-Header': 'custom-value' }), + () => (new Headers({ 'Authorization': 'Bearer token', 'Custom-Header': 'custom-value' })), + async () => (new Headers({ 'Authorization': 'Bearer token', 'Custom-Header': 'custom-value' })), + ] + it.each(headers)('should properly merge custom headers and return decoded data', async (headersFn) => { + const link = new ORPCLink({ + url: 'http://api.example.com', + headers: headersFn, + fetch: mockFetch, + payloadCodec: mockPayloadCodec as any, + }) + + const mockResponseData = { success: true, data: 'test data' } + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + }) + mockPayloadCodec.decode.mockResolvedValueOnce(mockResponseData) + + const result = await link.call(['test'], {}, { context: {} }) + + const headers = mockFetch.mock.calls[0]![1].headers + expect(headers.get('Authorization')).toBe('Bearer token') + expect(headers.get('Custom-Header')).toBe('custom-value') + expect(headers.get(ORPC_HANDLER_HEADER)).toBe(ORPC_HANDLER_VALUE) + + // Verify the result matches the decoded data + expect(result).toEqual(mockResponseData) + }) + + // Test URL encoding + it('should properly encode URL parameters and handle response', async () => { + const link = new ORPCLink({ + url: 'http://api.example.com/', + fetch: mockFetch, + payloadCodec: mockPayloadCodec as any, + }) + + const mockResponseData = { encoded: true, path: 'success' } + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + }) + mockPayloadCodec.decode.mockResolvedValueOnce(mockResponseData) + + const result = await link.call(['path with spaces', 'special/chars'], {}, { context: {} }) + + expect(mockFetch).toHaveBeenCalledWith( + 'http://api.example.com/path%20with%20spaces/special%2Fchars', + expect.any(Object), + expect.any(Object), + ) + + // Verify the result matches the decoded data + expect(result).toEqual(mockResponseData) + }) + + // Test error handling + it('should properly handle server errors', async () => { + const link = new ORPCLink({ + url: 'http://api.example.com', + fetch: mockFetch, + payloadCodec: mockPayloadCodec as any, + }) + + const errorResponse = new Response(JSON.stringify({ data: '__mocked__', meta: [] }), { + status: 500, + }) + + const errorData = { + code: 'CUSTOM_ERROR', + message: 'Custom error message', + status: 500, + } + + mockFetch.mockResolvedValue(errorResponse) + mockPayloadCodec.decode.mockResolvedValueOnce(errorData) + + await expect(link.call(['test'], {}, { context: {} })) + .rejects + .toThrow(ORPCError) + }) + + // Test with default payload codec + it('should use default ORPCPayloadCodec when none provided and return result', async () => { + const link = new ORPCLink({ + url: 'http://api.example.com', + fetch: mockFetch, + }) + + const mockResponseData = { defaultCodec: true } + mockFetch.mockResolvedValueOnce(new Response(JSON.stringify({ data: '__mocked__', meta: [] }))) + + const result = await link.call(['test'], {}, { context: {} }) + + expect(mockFetch).toHaveBeenCalled() + // Verify that it didn't use our mock codec + expect(mockPayloadCodec.encode).not.toHaveBeenCalled() + // The actual result would come from the default ORPCPayloadCodec + expect(result).toBeDefined() + }) + + it('should use default fetch when none provided', async () => { + const realFetch = globalThis.fetch + globalThis.fetch = mockFetch + + const link = new ORPCLink({ + url: 'http://api.example.com', + payloadCodec: mockPayloadCodec as any, + }) + + const mockResponse = new Response(JSON.stringify({ data: '__mocked__', meta: [] })) + mockFetch.mockReturnValue(mockResponse) + + const result = await link.call(['test'], {}, { context: {} }) + + expect(result).toEqual({ data: 'decoded-data' }) + expect(mockFetch).toHaveBeenCalledTimes(1) + expect(mockPayloadCodec.decode).toHaveBeenCalledTimes(1) + expect(mockPayloadCodec.decode).toHaveBeenCalledWith(mockResponse) + + globalThis.fetch = realFetch + }) + + // Test with AbortController signal + it('should properly handle AbortController signal and return result', async () => { + const link = new ORPCLink({ + url: 'http://api.example.com', + fetch: mockFetch, + payloadCodec: mockPayloadCodec as any, + }) + + const mockResponseData = { signal: 'handled' } + const abortController = new AbortController() + + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + }) + mockPayloadCodec.decode.mockResolvedValueOnce(mockResponseData) + + const result = await link.call(['test'], {}, { + context: {}, + signal: abortController.signal, + }) + + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + signal: abortController.signal, + }), + expect.any(Object), + ) + + expect(result).toEqual(mockResponseData) + }) + + it('should handle aborted requests properly', async () => { + const link = new ORPCLink({ + url: 'http://api.example.com', + fetch: mockFetch, + payloadCodec: mockPayloadCodec as any, + }) + + const abortController = new AbortController() + mockFetch.mockRejectedValueOnce(new DOMException('The operation was aborted', 'AbortError')) + + const promise = link.call(['test'], {}, { + context: {}, + signal: abortController.signal, + }) + + abortController.abort() + + await expect(promise).rejects.toThrow('The operation was aborted') + }) +}) diff --git a/packages/client/src/adapters/fetch/orpc-link.ts b/packages/client/src/adapters/fetch/orpc-link.ts new file mode 100644 index 00000000..4ba1800f --- /dev/null +++ b/packages/client/src/adapters/fetch/orpc-link.ts @@ -0,0 +1,63 @@ +import type { ProcedureClientOptions } from '@orpc/server' +import type { Promisable } from '@orpc/shared' +import type { ClientLink } from '../../types' +import type { FetchWithContext } from './types' +import { ORPCPayloadCodec, type PublicORPCPayloadCodec } from '@orpc/server/fetch' +import { ORPC_HANDLER_HEADER, ORPC_HANDLER_VALUE, trim } from '@orpc/shared' +import { ORPCError } from '@orpc/shared/error' + +export interface ORPCLinkOptions { + url: string + headers?: (input: unknown, context: TClientContext) => Promisable> + fetch?: FetchWithContext + payloadCodec?: PublicORPCPayloadCodec +} + +export class ORPCLink implements ClientLink { + private readonly fetch: FetchWithContext + private readonly payloadCodec: PublicORPCPayloadCodec + + constructor(private readonly options: ORPCLinkOptions) { + this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis) + this.payloadCodec = options.payloadCodec ?? new ORPCPayloadCodec() + } + + async call(path: readonly string[], input: unknown, options: ProcedureClientOptions): Promise { + const url = `${trim(this.options.url, '/')}/${path.map(encodeURIComponent).join('/')}` + const encoded = this.payloadCodec.encode(input) + const headers = new Headers(encoded.headers) + + headers.append(ORPC_HANDLER_HEADER, ORPC_HANDLER_VALUE) + + // clientContext only undefined when context is undefinable so we can safely cast it + const clientContext = options.context as typeof options.context & { context: TClientContext } + + let customHeaders = await this.options.headers?.(input, clientContext) + customHeaders = customHeaders instanceof Headers ? customHeaders : new Headers(customHeaders) + for (const [key, value] of customHeaders.entries()) { + headers.append(key, value) + } + + const response = await this.fetch(url, { + method: 'POST', + headers, + body: encoded.body, + signal: options.signal, + }, clientContext) + + const decoded = await this.payloadCodec.decode(response) + + if (!response.ok) { + const error = ORPCError.fromJSON(decoded) ?? new ORPCError({ + status: response.status, + code: 'INTERNAL_SERVER_ERROR', + message: 'Internal server error', + cause: decoded, + }) + + throw error + } + + return decoded + } +} diff --git a/packages/client/src/adapters/fetch/types.ts b/packages/client/src/adapters/fetch/types.ts new file mode 100644 index 00000000..ee50fb9f --- /dev/null +++ b/packages/client/src/adapters/fetch/types.ts @@ -0,0 +1,6 @@ +/// +/// + +export interface FetchWithContext { + (input: RequestInfo | URL, init: RequestInit | undefined, context: TClientContext): Promise +} diff --git a/packages/client/src/router-fetch-client.test-d.ts b/packages/client/src/client.test-d.ts similarity index 61% rename from packages/client/src/router-fetch-client.test-d.ts rename to packages/client/src/client.test-d.ts index 1ffa7c68..c0e65235 100644 --- a/packages/client/src/router-fetch-client.test-d.ts +++ b/packages/client/src/client.test-d.ts @@ -2,9 +2,9 @@ import type { ProcedureClient } from '@orpc/server' import { oc } from '@orpc/contract' import { os } from '@orpc/server' import { z } from 'zod' -import { createRouterFetchClient } from './router-fetch-client' +import { createORPCClient } from './client' -describe('router fetch client', () => { +describe('createORPCClient', () => { const pingContract = oc .input(z.object({ in: z.string() }).transform(i => i.in)) .output(z.string().transform(out => ({ out }))) @@ -28,20 +28,24 @@ describe('router fetch client', () => { }) it('build correct types with contract router', () => { - const client = createRouterFetchClient({ - baseURL: 'http://localhost:3000/orpc', - }) + const client = createORPCClient({} as any) - expectTypeOf(client.ping).toMatchTypeOf>() - expectTypeOf(client.nested.pong).toMatchTypeOf>() + expectTypeOf(client.ping).toMatchTypeOf>() + expectTypeOf(client.nested.pong).toMatchTypeOf>() }) it('build correct types with router', () => { - const client = createRouterFetchClient({ - baseURL: 'http://localhost:3000/orpc', - }) + const client = createORPCClient({} as any) - expectTypeOf(client.ping).toMatchTypeOf>() - expectTypeOf(client.nested.pong).toMatchTypeOf>() + expectTypeOf(client.ping).toMatchTypeOf>() + expectTypeOf(client.nested.pong).toMatchTypeOf>() + }) + + it('pass correct context', () => { + type Context = { a: number } + const client = createORPCClient({} as any) + + expectTypeOf(client.ping).toEqualTypeOf>() + expectTypeOf(client.nested.pong).toEqualTypeOf>() }) }) diff --git a/packages/client/src/client.test.ts b/packages/client/src/client.test.ts new file mode 100644 index 00000000..f94c4d7f --- /dev/null +++ b/packages/client/src/client.test.ts @@ -0,0 +1,48 @@ +import type { ClientLink } from './types' +import { createORPCClient } from './client' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('createORPCClient', () => { + const mockedLink: ClientLink = { + call: vi.fn().mockReturnValue('__mocked__'), + } + + it('works', async () => { + const client = createORPCClient(mockedLink) as any + + expect(await client.ping({ value: 'hello' })).toEqual('__mocked__') + expect(mockedLink.call).toBeCalledTimes(1) + expect(mockedLink.call).toBeCalledWith(['ping'], { value: 'hello' }, {}) + + vi.clearAllMocks() + expect(await client.nested.pong({ value: 'hello' })).toEqual('__mocked__') + expect(mockedLink.call).toBeCalledTimes(1) + expect(mockedLink.call).toBeCalledWith(['nested', 'pong'], { value: 'hello' }, {}) + }) + + it('works with signal', async () => { + const controller = new AbortController() + const signal = controller.signal + const client = createORPCClient(mockedLink) as any + + expect(await client.ping({ value: 'hello' }, { signal })).toEqual('__mocked__') + expect(mockedLink.call).toBeCalledTimes(1) + expect(mockedLink.call).toBeCalledWith(['ping'], { value: 'hello' }, { signal }) + }) + + it('works with context', async () => { + const client = createORPCClient(mockedLink) as any + + expect(await client.ping({ value: 'hello' }, { context: { userId: '123' } })).toEqual('__mocked__') + expect(mockedLink.call).toBeCalledTimes(1) + expect(mockedLink.call).toBeCalledWith(['ping'], { value: 'hello' }, { context: { userId: '123' } }) + }) + + it('not recursive on symbol', async () => { + const client = createORPCClient(mockedLink) as any + expect(client[Symbol('test')]).toBeUndefined() + }) +}) diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts new file mode 100644 index 00000000..3842099e --- /dev/null +++ b/packages/client/src/client.ts @@ -0,0 +1,36 @@ +import type { ContractRouter } from '@orpc/contract' +import type { ANY_ROUTER, ProcedureClient, RouterClient } from '@orpc/server' +import type { ClientLink } from './types' + +export interface createORPCClientOptions { + /** + * Use as base path for all procedure, useful when you only want to call a subset of the procedure. + */ + path?: string[] +} + +export function createORPCClient( + link: ClientLink, + options?: createORPCClientOptions, +): RouterClient { + const path = options?.path ?? [] + + const procedureClient: ProcedureClient = async (...[input, options]) => { + return await link.call(path, input, (options ?? {}) as Exclude) + } + + const recursive = new Proxy(procedureClient, { + get(target, key) { + if (typeof key !== 'string') { + return Reflect.get(target, key) + } + + return createORPCClient(link, { + ...options, + path: [...path, key], + }) + }, + }) + + return recursive as any +} diff --git a/packages/client/src/dynamic-link.test-d.ts b/packages/client/src/dynamic-link.test-d.ts new file mode 100644 index 00000000..889ce5ff --- /dev/null +++ b/packages/client/src/dynamic-link.test-d.ts @@ -0,0 +1,21 @@ +import type { ClientLink } from './types' +import { DynamicLink } from './dynamic-link' + +describe('dynamicLink', () => { + it('pass correct context', () => { + void new DynamicLink<{ batch?: boolean }>((path, input, options) => { + expectTypeOf(options.context).toEqualTypeOf<{ batch?: boolean } >() + + return {} as any + }) + }) + + it('required return a another link', () => { + void new DynamicLink(() => ({} as ClientLink)) + void new DynamicLink<{ batch?: boolean }>(() => ({} as ClientLink<{ batch?: boolean }>)) + // @ts-expect-error - context is mismatch + void new DynamicLink<{ batch?: boolean }>(() => ({} as ClientLink<{ batch?: string }>)) + // @ts-expect-error - must return a ClientLink + void new DynamicLink<{ batch?: boolean }>(() => ({})) + }) +}) diff --git a/packages/client/src/dynamic-link.test.ts b/packages/client/src/dynamic-link.test.ts new file mode 100644 index 00000000..665ad9b0 --- /dev/null +++ b/packages/client/src/dynamic-link.test.ts @@ -0,0 +1,26 @@ +import type { ProcedureClientOptions } from '@orpc/server' +import { describe, expect, it, vi } from 'vitest' +import { DynamicLink } from './dynamic-link' + +describe('dynamicLink', () => { + it('works', async () => { + const mockedLink = { call: vi.fn().mockResolvedValue('__mocked__') } + const mockLinkResolver = vi.fn().mockResolvedValue(mockedLink) + const link = new DynamicLink(mockLinkResolver) + + const path = ['users', 'getProfile'] + const input = { id: 123 } + const options: ProcedureClientOptions = { + context: { + batch: true, + }, + } + + expect(await link.call(path, input, options)).toEqual('__mocked__') + + expect(mockLinkResolver).toHaveBeenCalledTimes(1) + expect(mockLinkResolver).toHaveBeenCalledWith(path, input, options) + expect(mockedLink.call).toHaveBeenCalledTimes(1) + expect(mockedLink.call).toHaveBeenCalledWith(path, input, options) + }) +}) diff --git a/packages/client/src/dynamic-link.ts b/packages/client/src/dynamic-link.ts new file mode 100644 index 00000000..8c2c584c --- /dev/null +++ b/packages/client/src/dynamic-link.ts @@ -0,0 +1,27 @@ +import type { ProcedureClientOptions } from '@orpc/server' +import type { Promisable } from '@orpc/shared' +import type { ClientLink } from './types' + +/** + * DynamicLink provides a way to dynamically resolve and delegate calls to other ClientLinks + * based on the request path, input, and context. + */ +export class DynamicLink implements ClientLink { + constructor( + private readonly linkResolver: ( + path: readonly string[], + input: unknown, + options: ProcedureClientOptions & { context: TClientContext }, + ) => Promisable>, + ) { + } + + async call(path: readonly string[], input: unknown, options: ProcedureClientOptions): Promise { + // Since the context is only optional when the context is undefinable, we can safely cast it + const resolvedLink = await this.linkResolver(path, input, options as typeof options & { context: TClientContext }) + + const output = await resolvedLink.call(path, input, options) + + return output + } +} diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 90be1887..547a4506 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -1,9 +1,7 @@ /** unnoq */ -import { createRouterFetchClient } from './router-fetch-client' +export * from './client' +export * from './dynamic-link' +export * from './types' -export * from './procedure-fetch-client' -export * from './router-fetch-client' export * from '@orpc/shared/error' - -export const createORPCFetchClient = createRouterFetchClient diff --git a/packages/client/src/procedure-fetch-client.test-d.ts b/packages/client/src/procedure-fetch-client.test-d.ts deleted file mode 100644 index 54598e13..00000000 --- a/packages/client/src/procedure-fetch-client.test-d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { ProcedureClient } from '@orpc/server' -import { createProcedureFetchClient } from './procedure-fetch-client' - -describe('procedure fetch client', () => { - it('just a client', () => { - const client = createProcedureFetchClient({ - baseURL: 'http://localhost:3000/orpc', - path: ['ping'], - }) - - expectTypeOf(client).toEqualTypeOf>() - }) -}) diff --git a/packages/client/src/procedure-fetch-client.test.ts b/packages/client/src/procedure-fetch-client.test.ts deleted file mode 100644 index 6d7a712e..00000000 --- a/packages/client/src/procedure-fetch-client.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { ORPCPayloadCodec } from '@orpc/server/fetch' -import { createProcedureFetchClient } from './procedure-fetch-client' - -vi.mock('@orpc/server/fetch', () => ({ - ORPCPayloadCodec: vi.fn().mockReturnValue({ encode: vi.fn(), decode: vi.fn() }), -})) - -beforeEach(() => { - vi.clearAllMocks() -}) - -describe('procedure fetch client', () => { - const encode = (ORPCPayloadCodec as any)().encode - const decode = (ORPCPayloadCodec as any)().decode - const response = new Response('output') - const headers = new Headers({ 'Content-Type': 'application/json' }) - - const fakeFetch = vi.fn() - fakeFetch.mockReturnValue(response) - encode.mockReturnValue({ body: 'transformed_input', headers }) - decode.mockReturnValue('transformed_output') - - it('works', async () => { - const client = createProcedureFetchClient({ - baseURL: 'http://localhost:3000/orpc', - path: ['ping'], - fetch: fakeFetch, - }) - - const output = await client('input') - - expect(output).toBe('transformed_output') - - expect(encode).toBeCalledTimes(1) - expect(encode).toBeCalledWith('input') - - expect(fakeFetch).toBeCalledTimes(1) - expect(fakeFetch).toBeCalledWith('http://localhost:3000/orpc/ping', { - method: 'POST', - body: 'transformed_input', - headers: expect.any(Headers), - }) - - expect(decode).toBeCalledTimes(1) - expect(decode).toBeCalledWith(response) - }) - - it.each([ - async () => new Headers({ 'x-test': 'hello' }), - async () => ({ 'x-test': 'hello' }), - ])('works with headers', async (headers) => { - const client = createProcedureFetchClient({ - path: ['ping'], - baseURL: 'http://localhost:3000/orpc', - fetch: fakeFetch, - headers, - }) - - await client({ value: 'hello' }) - - expect(fakeFetch).toBeCalledWith('http://localhost:3000/orpc/ping', { - method: 'POST', - body: 'transformed_input', - headers: expect.any(Headers), - }) - - expect(fakeFetch.mock.calls[0]![1].headers.get('x-test')).toBe('hello') - }) - - it('abort signal', async () => { - const controller = new AbortController() - const signal = controller.signal - - const client = createProcedureFetchClient({ - path: ['ping'], - baseURL: 'http://localhost:3000/orpc', - fetch: fakeFetch, - }) - - await client(undefined, { signal }) - - expect(fakeFetch).toBeCalledTimes(1) - expect(fakeFetch.mock.calls[0]![1].signal).toBe(signal) - }) -}) diff --git a/packages/client/src/procedure-fetch-client.ts b/packages/client/src/procedure-fetch-client.ts deleted file mode 100644 index 4eaddc8a..00000000 --- a/packages/client/src/procedure-fetch-client.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { ProcedureClient } from '@orpc/server' -import type { Promisable } from '@orpc/shared' -import { ORPCPayloadCodec } from '@orpc/server/fetch' -import { ORPC_HANDLER_HEADER, ORPC_HANDLER_VALUE, trim } from '@orpc/shared' -import { ORPCError } from '@orpc/shared/error' - -export interface CreateProcedureClientOptions { - /** - * The base url of the server. - */ - baseURL: string - - /** - * The fetch function used to make the request. - * @default global fetch - */ - fetch?: typeof fetch - - /** - * The headers used to make the request. - * Invoked before the request is made. - */ - headers?: (input: unknown) => Promisable> - - /** - * The path of the procedure on server. - */ - path: string[] -} - -const payloadCodec = new ORPCPayloadCodec() - -export function createProcedureFetchClient( - options: CreateProcedureClientOptions, -): ProcedureClient { - const client: ProcedureClient = async (...[input, callerOptions]) => { - const fetchClient = options.fetch ?? fetch - const url = `${trim(options.baseURL, '/')}/${options.path.map(encodeURIComponent).join('/')}` - - const encoded = payloadCodec.encode(input) - - const headers = new Headers(encoded.headers) - - headers.append(ORPC_HANDLER_HEADER, ORPC_HANDLER_VALUE) - - let customHeaders = await options.headers?.(input) - customHeaders = customHeaders instanceof Headers ? customHeaders : new Headers(customHeaders) - for (const [key, value] of customHeaders.entries()) { - headers.append(key, value) - } - - const response = await fetchClient(url, { - method: 'POST', - headers, - body: encoded.body, - signal: callerOptions?.signal, - }) - - const json = await (async () => { - try { - return await payloadCodec.decode(response) - } - catch (e) { - throw new ORPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: 'Cannot parse response.', - cause: e, - }) - } - })() - - if (!response.ok) { - throw ( - ORPCError.fromJSON(json) - ?? new ORPCError({ - status: response.status, - code: 'INTERNAL_SERVER_ERROR', - message: 'Internal server error', - }) - ) - } - - return json as any - } - - return client -} diff --git a/packages/client/src/router-fetch-client.test.ts b/packages/client/src/router-fetch-client.test.ts deleted file mode 100644 index 780fe416..00000000 --- a/packages/client/src/router-fetch-client.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { createProcedureFetchClient } from './procedure-fetch-client' -import { createRouterFetchClient } from './router-fetch-client' - -vi.mock('./procedure-fetch-client', () => ({ - createProcedureFetchClient: vi.fn(), -})) - -beforeEach(() => { - vi.clearAllMocks() -}) - -describe('router fetch client', () => { - const procedureClient = vi.fn().mockReturnValue('__mocked__') - vi.mocked(createProcedureFetchClient).mockReturnValue(procedureClient) - - it('works', async () => { - const client = createRouterFetchClient({ - baseURL: 'http://localhost:3000/orpc', - }) as any - - vi.clearAllMocks() - const o1 = await client.ping({ value: 'hello' }) - - expect(o1).toEqual('__mocked__') - expect(createProcedureFetchClient).toBeCalledTimes(1) - expect(createProcedureFetchClient).toBeCalledWith({ - baseURL: 'http://localhost:3000/orpc', - path: ['ping'], - }) - - vi.clearAllMocks() - const o2 = await client.nested.pong({ value: 'hello' }) - - expect(o2).toEqual('__mocked__') - expect(createProcedureFetchClient).toBeCalledTimes(2) - expect(createProcedureFetchClient).toBeCalledWith({ - baseURL: 'http://localhost:3000/orpc', - path: ['nested', 'pong'], - }) - }) - - it('works with options', async () => { - const headers = vi.fn() - const fetch = vi.fn() - const client = createRouterFetchClient({ - baseURL: 'http://localhost:3000/orpc', - path: ['base'], - headers, - fetch, - }) as any - - vi.clearAllMocks() - await client.ping({ value: 'hello' }) - - expect(createProcedureFetchClient).toBeCalledTimes(1) - expect(createProcedureFetchClient).toBeCalledWith({ - baseURL: 'http://localhost:3000/orpc', - path: ['base', 'ping'], - headers, - fetch, - }) - }) - - it('not recursive on symbol', async () => { - const client = createRouterFetchClient({ - baseURL: 'http://localhost:3000/orpc', - }) as any - - expect(client[Symbol('test')]).toBeUndefined() - }) -}) diff --git a/packages/client/src/router-fetch-client.ts b/packages/client/src/router-fetch-client.ts deleted file mode 100644 index 9cb92be8..00000000 --- a/packages/client/src/router-fetch-client.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { ContractRouter } from '@orpc/contract' -import type { ANY_ROUTER, RouterClient } from '@orpc/server' -import type { SetOptional } from '@orpc/shared' -import type { CreateProcedureClientOptions } from './procedure-fetch-client' -import { createProcedureFetchClient } from './procedure-fetch-client' - -export function createRouterFetchClient( - options: SetOptional, -): RouterClient { - const path = options?.path ?? [] - - const client = new Proxy( - createProcedureFetchClient({ - ...options, - path, - }), - { - get(target, key) { - if (typeof key !== 'string') { - return Reflect.get(target, key) - } - - return createRouterFetchClient({ - ...options, - path: [...path, key], - }) - }, - }, - ) - - return client as any -} diff --git a/packages/client/src/types.ts b/packages/client/src/types.ts new file mode 100644 index 00000000..9a0c5bc8 --- /dev/null +++ b/packages/client/src/types.ts @@ -0,0 +1,5 @@ +import type { ProcedureClientOptions } from '@orpc/server' + +export interface ClientLink { + call: (path: readonly string[], input: unknown, options: ProcedureClientOptions) => Promise +} diff --git a/packages/client/tests/helpers.ts b/packages/client/tests/helpers.ts index 44dc6396..d7412a8d 100644 --- a/packages/client/tests/helpers.ts +++ b/packages/client/tests/helpers.ts @@ -1,7 +1,8 @@ import { os } from '@orpc/server' import { ORPCHandler } from '@orpc/server/fetch' import { z } from 'zod' -import { createORPCFetchClient } from '../src' +import { createORPCClient } from '../src' +import { ORPCLink } from '../src/adapters/fetch' export const orpcServer = os @@ -98,13 +99,15 @@ export const appRouter = orpcServer.router({ const orpcHandler = new ORPCHandler(appRouter) -export const client = createORPCFetchClient({ - baseURL: 'http://localhost:3000', +const orpcLink = new ORPCLink({ + url: 'http://localhost:3000', - async fetch(...args) { + async fetch(input, init) { await new Promise(resolve => setTimeout(resolve, 100)) - const request = new Request(...args) + const request = new Request(input, init) return orpcHandler.fetch(request) }, }) + +export const client = createORPCClient(orpcLink) diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index ce74d7ff..c13ca867 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.lib.json", "compilerOptions": { - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": ["ES2020"], "types": [] }, "references": [ diff --git a/packages/next/src/action-safe.ts b/packages/next/src/action-safe.ts index 52c98809..14ceeaf4 100644 --- a/packages/next/src/action-safe.ts +++ b/packages/next/src/action-safe.ts @@ -6,7 +6,8 @@ export type SafeAction = ProcedureClient< TInput, | [TOutput, undefined, 'success'] - | [undefined, WELL_ORPC_ERROR_JSON, 'error'] + | [undefined, WELL_ORPC_ERROR_JSON, 'error'], + unknown > export function createSafeAction< @@ -21,7 +22,7 @@ export function createSafeAction< const safeAction: SafeAction, SchemaOutput> = async (...[input, option]) => { try { - const output = await caller(input as any, option) + const output = await caller(input as any, option as any) return [output as any, undefined, 'success'] } catch (e) { diff --git a/packages/next/src/client/action-hooks.ts b/packages/next/src/client/action-hooks.ts index a6fac320..f5804837 100644 --- a/packages/next/src/client/action-hooks.ts +++ b/packages/next/src/client/action-hooks.ts @@ -23,7 +23,7 @@ export type UseActionState = { const idleState = { status: 'idle', isPending: false, isError: false, input: undefined, output: undefined, error: undefined } as const export function useAction( - action: ProcedureClient, + action: ProcedureClient, hooks?: Hooks, ): UseActionState { const [state, setState] = useState, 'execute' | 'reset'>>(idleState) diff --git a/packages/next/src/client/action-safe-hooks.ts b/packages/next/src/client/action-safe-hooks.ts index 45aa2b8d..972089aa 100644 --- a/packages/next/src/client/action-safe-hooks.ts +++ b/packages/next/src/client/action-safe-hooks.ts @@ -9,7 +9,7 @@ export function useSafeAction( action: SafeAction, hooks?: Hooks, ): UseActionState { - const normal: ProcedureClient = useCallback(async (...args) => { + const normal: ProcedureClient = useCallback(async (...args) => { const [output, errorJson, status] = await action(...args) if (status === 'error') { diff --git a/packages/react-query/src/types.ts b/packages/react-query/src/types.ts index d103ff6c..9c56609a 100644 --- a/packages/react-query/src/types.ts +++ b/packages/react-query/src/types.ts @@ -9,15 +9,23 @@ import type { export type InferCursor = T extends { cursor?: any } ? T['cursor'] : never -export type QueryOptions = (undefined extends TInput ? { input?: TInput } : { input: TInput }) & ( +export type QueryOptions = +& (undefined extends TInput ? { input?: TInput } : { input: TInput }) +& (undefined extends TClientContext ? { context?: TClientContext } : { context: TClientContext }) +& ( SetOptional, 'queryKey'> ) -export type InfiniteOptions = (undefined extends TInput ? { input?: Omit } : { input: Omit }) & ( +export type InfiniteOptions = +& (undefined extends TInput ? { input?: Omit } : { input: Omit }) +& (undefined extends TClientContext ? { context?: TClientContext } : { context: TClientContext }) +& ( SetOptional< UndefinedInitialDataInfiniteOptions>, 'queryKey' | (undefined extends InferCursor ? 'initialPageParam' : never) > ) -export type MutationOptions = UseMutationOptions +export type MutationOptions = + & (undefined extends TClientContext ? { context?: TClientContext } : { context: TClientContext }) + & UseMutationOptions diff --git a/packages/react-query/src/utils-procedure.test-d.ts b/packages/react-query/src/utils-procedure.test-d.ts index dea2bbf0..a055a99e 100644 --- a/packages/react-query/src/utils-procedure.test-d.ts +++ b/packages/react-query/src/utils-procedure.test-d.ts @@ -1,15 +1,16 @@ import type { ProcedureClient } from '@orpc/server' import type { InfiniteData, QueryKey } from '@tanstack/react-query' -import { useInfiniteQuery, useQuery } from '@tanstack/react-query' +import type { ProcedureUtils } from './utils-procedure' +import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query' import { createProcedureUtils } from './utils-procedure' describe('queryOptions', () => { - const client = vi.fn>( + const client = vi.fn>( (...[input]) => Promise.resolve(input?.toString()), ) const utils = createProcedureUtils(client, []) - const client2 = vi.fn((input: number) => Promise.resolve(input.toString())) + const client2 = {} as ProcedureClient const utils2 = createProcedureUtils(client2, []) it('infer correct input type', () => { @@ -51,13 +52,36 @@ describe('queryOptions', () => { expectTypeOf(query.data).toEqualTypeOf<12344>() } }) + + describe('client context', () => { + it('can be optional', () => { + const utils = {} as ProcedureUtils + useQuery(utils.queryOptions()) + useQuery(utils.queryOptions({})) + useQuery(utils.queryOptions({ context: undefined })) + useQuery(utils.queryOptions({ context: { batch: true } })) + // @ts-expect-error --- invalid context + useQuery(utils.queryOptions({ context: { batch: 'invalid' } })) + }) + + it('required pass context when non-optional', () => { + const utils = {} as ProcedureUtils + // @ts-expect-error --- missing context + useQuery(utils.queryOptions()) + // @ts-expect-error --- missing context + useQuery(utils.queryOptions({})) + useQuery(utils.queryOptions({ context: { batch: true } })) + // @ts-expect-error --- invalid context + useQuery(utils.queryOptions({ context: { batch: 123 } })) + }) + }) }) describe('infiniteOptions', () => { const getNextPageParam = vi.fn() it('cannot use on procedure without input object-able', () => { - const utils = createProcedureUtils({} as (input: number) => Promise, []) + const utils = createProcedureUtils({} as ProcedureClient, []) // @ts-expect-error missing initialPageParam utils.infiniteOptions({ @@ -80,7 +104,7 @@ describe('infiniteOptions', () => { }) it('infer correct input type', () => { - const utils = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, []) + const utils = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor: number }, string, unknown>, []) utils.infiniteOptions({ input: { @@ -111,7 +135,7 @@ describe('infiniteOptions', () => { }) it('infer correct initialPageParam type', () => { - const utils = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, []) + const utils = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor: number }, string, unknown>, []) utils.infiniteOptions({ input: {}, @@ -134,14 +158,14 @@ describe('infiniteOptions', () => { }) it('initialPageParam can be optional', () => { - const utils = createProcedureUtils({} as (input: { limit?: number, cursor?: number }) => Promise, []) + const utils = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor?: number }, string, unknown>, []) utils.infiniteOptions({ input: {}, getNextPageParam, }) - const utils2 = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, []) + const utils2 = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor: number }, string, unknown>, []) // @ts-expect-error initialPageParam is required utils2.infiniteOptions({ @@ -151,13 +175,13 @@ describe('infiniteOptions', () => { }) it('input can be optional', () => { - const utils = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor?: number } | undefined, string>, []) + const utils = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor?: number } | undefined, string, unknown>, []) utils.infiniteOptions({ getNextPageParam, }) - const utils2 = createProcedureUtils({} as (input: { limit?: number, cursor?: number }) => Promise, []) + const utils2 = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor?: number }, string, unknown>, []) // @ts-expect-error input is required utils2.infiniteOptions({ @@ -166,7 +190,7 @@ describe('infiniteOptions', () => { }) it('infer correct output type', () => { - const utils = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, []) + const utils = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor: number }, string, unknown>, []) const query = useInfiniteQuery(utils.infiniteOptions({ input: { limit: 1, @@ -181,7 +205,7 @@ describe('infiniteOptions', () => { }) it('work with select options', () => { - const utils = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, []) + const utils = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor: number }, string, unknown>, []) const query = useInfiniteQuery(utils.infiniteOptions({ input: { limit: 1, @@ -199,10 +223,38 @@ describe('infiniteOptions', () => { expectTypeOf(query.data).toEqualTypeOf<{ value: string }>() } }) + + describe('client context', () => { + it('can be optional', () => { + const utils = {} as ProcedureUtils + + const getNextPageParam = vi.fn() + const initialPageParam = 1 + + useInfiniteQuery(utils.infiniteOptions({ getNextPageParam, initialPageParam })) + useInfiniteQuery(utils.infiniteOptions({ getNextPageParam, initialPageParam, context: undefined })) + useInfiniteQuery(utils.infiniteOptions({ getNextPageParam, initialPageParam, context: { batch: true } })) + // @ts-expect-error --- invalid context + useInfiniteQuery(utils.infiniteOptions({ getNextPageParam, initialPageParam, context: { batch: 'invalid' } })) + }) + + it('required pass context when non-optional', () => { + const utils = {} as ProcedureUtils + + const getNextPageParam = vi.fn() + const initialPageParam = 1 + + // @ts-expect-error --- missing context + useInfiniteQuery(utils.infiniteOptions({ getNextPageParam, initialPageParam })) + useInfiniteQuery(utils.infiniteOptions({ getNextPageParam, initialPageParam, context: { batch: true } })) + // @ts-expect-error --- invalid context + useInfiniteQuery(utils.infiniteOptions({ getNextPageParam, initialPageParam, context: { batch: 'invalid' } })) + }) + }) }) describe('mutationOptions', () => { - const client = vi.fn((input: number) => Promise.resolve(input.toString())) + const client = {} as ProcedureClient const utils = createProcedureUtils(client, []) it('infer correct input type', () => { @@ -237,4 +289,27 @@ describe('mutationOptions', () => { expectTypeOf(option.mutationKey).toEqualTypeOf() expectTypeOf(option.mutationFn).toEqualTypeOf<(input: number) => Promise>() }) + + describe('client context', () => { + it('can be optional', () => { + const utils = {} as ProcedureUtils + useMutation(utils.mutationOptions()) + useMutation(utils.mutationOptions({})) + useMutation(utils.mutationOptions({ context: undefined })) + useMutation(utils.mutationOptions({ context: { batch: true } })) + // @ts-expect-error --- invalid context + useMutation(utils.mutationOptions({ context: { batch: 'invalid' } })) + }) + + it('required pass context when non-optional', () => { + const utils = {} as ProcedureUtils + // @ts-expect-error --- missing context + useMutation(utils.mutationOptions()) + // @ts-expect-error --- missing context + useMutation(utils.mutationOptions({})) + useMutation(utils.mutationOptions({ context: { batch: true } })) + // @ts-expect-error --- invalid context + useMutation(utils.mutationOptions({ context: { batch: 123 } })) + }) + }) }) diff --git a/packages/react-query/src/utils-procedure.test.ts b/packages/react-query/src/utils-procedure.test.ts index 23ab5fd9..1e739f32 100644 --- a/packages/react-query/src/utils-procedure.test.ts +++ b/packages/react-query/src/utils-procedure.test.ts @@ -1,4 +1,3 @@ -import type { ProcedureClient } from '@orpc/server' import * as keyModule from './key' import { createProcedureUtils } from './utils-procedure' @@ -12,7 +11,7 @@ beforeEach(() => { }) describe('queryOptions', () => { - const client = vi.fn>((...[input]) => Promise.resolve(input?.toString())) + const client = vi.fn((...[input]) => Promise.resolve(input?.toString())) const utils = createProcedureUtils(client, ['ping']) it('works', async () => { @@ -27,13 +26,29 @@ describe('queryOptions', () => { expect(client).toHaveBeenCalledTimes(1) expect(client).toBeCalledWith(1, { signal }) }) + + it('works with client context', async () => { + const client = vi.fn((...[input]) => Promise.resolve(input?.toString())) + const utils = createProcedureUtils(client, ['ping']) + + const options = utils.queryOptions({ context: { batch: true } }) + + expect(options.queryKey).toEqual(['__ORPC__', ['ping'], { type: 'query' }]) + expect(buildKeySpy).toHaveBeenCalledTimes(1) + expect(buildKeySpy).toHaveBeenCalledWith(['ping'], { type: 'query' }) + + client.mockResolvedValueOnce('__mocked__') + await expect((options as any).queryFn({ signal })).resolves.toEqual('__mocked__') + expect(client).toHaveBeenCalledTimes(1) + expect(client).toBeCalledWith(undefined, { signal, context: { batch: true } }) + }) }) describe('infiniteOptions', () => { const getNextPageParam = vi.fn() it('works ', async () => { - const client = vi.fn<(input: { limit?: number, cursor: number | undefined }) => Promise>() + const client = vi.fn() const utils = createProcedureUtils(client, []) const options = utils.infiniteOptions({ @@ -54,7 +69,7 @@ describe('infiniteOptions', () => { }) it('works without initialPageParam', async () => { - const client = vi.fn<(input: { limit?: number, cursor: number | undefined }) => Promise>() + const client = vi.fn() const utils = createProcedureUtils(client, []) const options = utils.infiniteOptions({ @@ -71,11 +86,31 @@ describe('infiniteOptions', () => { expect(client).toHaveBeenCalledTimes(1) expect(client).toBeCalledWith({ limit: 5, cursor: undefined }, { signal }) }) + + it('works with client context', async () => { + const client = vi.fn() + const utils = createProcedureUtils(client, []) + + const options = utils.infiniteOptions({ + context: { batch: true }, + getNextPageParam, + initialPageParam: 1, + }) + + expect(options.queryKey).toEqual(['__ORPC__', [], { type: 'infinite' }]) + expect(buildKeySpy).toHaveBeenCalledTimes(1) + expect(buildKeySpy).toHaveBeenCalledWith([], { type: 'infinite' }) + + client.mockResolvedValueOnce('__mocked__') + await expect((options as any).queryFn({ pageParam: 1, signal })).resolves.toEqual('__mocked__') + expect(client).toHaveBeenCalledTimes(1) + expect(client).toBeCalledWith({ limit: undefined, cursor: 1 }, { signal, context: { batch: true } }) + }) }) describe('mutationOptions', () => { it('works', async () => { - const client = vi.fn>( + const client = vi.fn( (...[input]) => Promise.resolve(input?.toString()), ) const utils = createProcedureUtils(client, ['ping']) @@ -89,6 +124,24 @@ describe('mutationOptions', () => { client.mockResolvedValueOnce('__mocked__') await expect(options.mutationFn(1)).resolves.toEqual('__mocked__') expect(client).toHaveBeenCalledTimes(1) - expect(client).toBeCalledWith(1) + expect(client).toBeCalledWith(1, {}) + }) + + it('works with client context', async () => { + const client = vi.fn( + (...[input]) => Promise.resolve(input?.toString()), + ) + const utils = createProcedureUtils(client, ['ping']) + + const options = utils.mutationOptions({ context: { batch: true } }) + + expect(options.mutationKey).toEqual(['__ORPC__', ['ping'], { type: 'mutation' }]) + expect(buildKeySpy).toHaveBeenCalledTimes(1) + expect(buildKeySpy).toHaveBeenCalledWith(['ping'], { type: 'mutation' }) + + client.mockResolvedValueOnce('__mocked__') + await expect(options.mutationFn(1)).resolves.toEqual('__mocked__') + expect(client).toHaveBeenCalledTimes(1) + expect(client).toBeCalledWith(1, { context: { batch: true } }) }) }) diff --git a/packages/react-query/src/utils-procedure.ts b/packages/react-query/src/utils-procedure.ts index 6bf69a22..d98875f4 100644 --- a/packages/react-query/src/utils-procedure.ts +++ b/packages/react-query/src/utils-procedure.ts @@ -7,35 +7,35 @@ import { buildKey } from './key' /** * Utils at procedure level */ -export interface ProcedureUtils { - queryOptions: >( - ...opts: [options: U] | (undefined extends TInput ? [] : never) - ) => IsEqual> extends true +export interface ProcedureUtils { + queryOptions: >( + ...opts: [options: U] | (undefined extends TInput & TClientContext ? [] : never) + ) => IsEqual> extends true ? { queryKey: QueryKey, queryFn: () => Promise } : Omit<{ queryKey: QueryKey, queryFn: () => Promise }, keyof U> & U - infiniteOptions: >( + infiniteOptions: >( options: U ) => Omit<{ queryKey: QueryKey, queryFn: () => Promise, initialPageParam: undefined }, keyof U> & U - mutationOptions: >( - options?: U - ) => IsEqual> extends true + mutationOptions: >( + ...opt: [options: U] | (undefined extends TClientContext ? [] : never) + ) => IsEqual> extends true ? { mutationKey: QueryKey, mutationFn: (input: TInput) => Promise } : Omit<{ mutationKey: QueryKey, mutationFn: (input: TInput) => Promise }, keyof U> & U } -export function createProcedureUtils( - client: ProcedureClient, +export function createProcedureUtils( + client: ProcedureClient, path: string[], -): ProcedureUtils { +): ProcedureUtils { return { queryOptions(...[options]) { const input = options?.input as any return { queryKey: buildKey(path, { type: 'query', input }), - queryFn: ({ signal }) => client(input, { signal }), + queryFn: ({ signal }) => client(input, { signal, context: options?.context } as any), ...(options as any), } }, @@ -45,15 +45,15 @@ export function createProcedureUtils( return { queryKey: buildKey(path, { type: 'infinite', input }), - queryFn: ({ pageParam, signal }) => client({ ...input, cursor: pageParam }, { signal }), + queryFn: ({ pageParam, signal }) => client({ ...input, cursor: pageParam }, { signal, context: options.context } as any), ...(options as any), } }, - mutationOptions(options) { + mutationOptions(...[options]) { return { mutationKey: buildKey(path, { type: 'mutation' }), - mutationFn: input => client(input), + mutationFn: input => client(input, { context: options?.context } as any), ...(options as any), } }, diff --git a/packages/react-query/src/utils-router.test-d.ts b/packages/react-query/src/utils-router.test-d.ts index 488ec9b7..be71ea49 100644 --- a/packages/react-query/src/utils-router.test-d.ts +++ b/packages/react-query/src/utils-router.test-d.ts @@ -1,4 +1,6 @@ import type { RouterClient } from '@orpc/server' +import type { GeneralUtils } from './utils-general' +import type { ProcedureUtils } from './utils-procedure' import { oc } from '@orpc/contract' import { os } from '@orpc/server' import { z } from 'zod' @@ -23,7 +25,7 @@ const router = os.contract(contractRouter).router({ describe('with contract router', () => { it('build correct types', () => { - const utils = createRouterUtils({} as RouterClient) + const utils = createRouterUtils({} as RouterClient) const generalUtils = createGeneralUtils([]) const pingUtils = createProcedureUtils(ping, []) @@ -41,7 +43,7 @@ describe('with contract router', () => { describe('with router', () => { it('build correct types', () => { - const utils = createRouterUtils({} as RouterClient) + const utils = createRouterUtils({} as RouterClient) const generalUtils = createGeneralUtils([]) const pingUtils = createProcedureUtils(ping, []) @@ -56,3 +58,19 @@ describe('with router', () => { expectTypeOf(utils.pong).toMatchTypeOf() }) }) + +it('with client context', () => { + const utils = createRouterUtils({} as RouterClient) + + const generalUtils = {} as GeneralUtils + const pingUtils = {} as ProcedureUtils<{ name: string }, string, undefined | { batch?: boolean }> + const pingGeneralUtils = createGeneralUtils<{ name: string }>(['ping']) + const pongUtils = {} as ProcedureUtils + const pongGeneralUtils = {} as GeneralUtils + + expectTypeOf(utils).toMatchTypeOf() + expectTypeOf(utils.ping).toMatchTypeOf() + expectTypeOf(utils.ping).toMatchTypeOf() + expectTypeOf(utils.pong).toMatchTypeOf() + expectTypeOf(utils.pong).toMatchTypeOf() +}) diff --git a/packages/react-query/src/utils-router.ts b/packages/react-query/src/utils-router.ts index b352410b..30acc3ea 100644 --- a/packages/react-query/src/utils-router.ts +++ b/packages/react-query/src/utils-router.ts @@ -2,18 +2,18 @@ import type { ProcedureClient, RouterClient } from '@orpc/server' import { createGeneralUtils, type GeneralUtils } from './utils-general' import { createProcedureUtils, type ProcedureUtils } from './utils-procedure' -export type RouterUtils> = -T extends ProcedureClient - ? ProcedureUtils & GeneralUtils +export type RouterUtils> = +T extends ProcedureClient + ? ProcedureUtils & GeneralUtils : { - [K in keyof T]: T[K] extends RouterClient ? RouterUtils : never + [K in keyof T]: T[K] extends RouterClient ? RouterUtils : never } & GeneralUtils /** * @param client - The client create form `@orpc/client` * @param path - The base path for query key */ -export function createRouterUtils>( +export function createRouterUtils>( client: T, path: string[] = [], ): RouterUtils { diff --git a/packages/react-query/tests/e2e.test-d.ts b/packages/react-query/tests/e2e.test-d.ts index 50f2971f..15a7f151 100644 --- a/packages/react-query/tests/e2e.test-d.ts +++ b/packages/react-query/tests/e2e.test-d.ts @@ -28,6 +28,13 @@ describe('useQuery', () => { // @ts-expect-error input is invalid useQuery(orpc.user.find.queryOptions({ input: { id: 123 } })) }) + + it('infer types correctly with client context', async () => { + useQuery(orpc.user.find.queryOptions({ input: { id: '123' } })) + useQuery(orpc.user.find.queryOptions({ input: { id: '123' }, context: { batch: true } })) + // @ts-expect-error --- invalid context + useQuery(orpc.user.find.queryOptions({ input: { id: '123' }, context: { batch: 'invalid' } })) + }) }) describe('useInfiniteQuery', () => { @@ -84,6 +91,25 @@ describe('useInfiniteQuery', () => { getNextPageParam: {} as any, })) }) + + it('infer types correctly with client context', async () => { + useInfiniteQuery(orpc.user.list.infiniteOptions({ + input: { keyword: 'keyword' }, + getNextPageParam: {} as any, + })) + useInfiniteQuery(orpc.user.list.infiniteOptions({ + input: { keyword: 'keyword' }, + getNextPageParam: {} as any, + context: { batch: true }, + })) + // @ts-expect-error --- invalid context + useInfiniteQuery(orpc.user.list.infiniteOptions({ + input: { keyword: 'keyword' }, + getNextPageParam: {} as any, + // @ts-expect-error --- invalid context + context: { batch: 'invalid' }, + })) + }) }) describe('useMutation', () => { @@ -98,6 +124,13 @@ describe('useMutation', () => { expectTypeOf(query.mutateAsync).toMatchTypeOf<(input: { id: string }) => Promise<{ id: string, name: string }>>() }) + + it('infer types correctly with client context', async () => { + useMutation(orpc.user.find.mutationOptions(({}))) + useMutation(orpc.user.find.mutationOptions(({ context: { batch: true } }))) + // @ts-expect-error --- invalid context + useMutation(orpc.user.find.mutationOptions(({ context: { batch: 'invalid' } }))) + }) }) describe('useQueries', () => { @@ -163,4 +196,23 @@ describe('useQueries', () => { ], }) }) + + it('infer types correctly with client context', async () => { + useQueries({ + queries: [ + orpc.user.find.queryOptions({ + input: { id: '0' }, + }), + orpc.user.find.queryOptions({ + input: { id: '0' }, + context: { batch: true }, + }), + orpc.user.find.queryOptions({ + input: { id: '0' }, + // @ts-expect-error --- invalid context + context: { batch: 'invalid' }, + }), + ], + }) + }) }) diff --git a/packages/react-query/tests/helpers.tsx b/packages/react-query/tests/helpers.tsx index 3214cce3..8d4d66ae 100644 --- a/packages/react-query/tests/helpers.tsx +++ b/packages/react-query/tests/helpers.tsx @@ -1,4 +1,5 @@ -import { createORPCFetchClient } from '@orpc/client' +import { createORPCClient } from '@orpc/client' +import { ORPCLink } from '@orpc/client/fetch' import { os } from '@orpc/server' import { ORPCHandler } from '@orpc/server/fetch' import { QueryClient } from '@tanstack/react-query' @@ -93,17 +94,19 @@ export const appRouter = orpcServer.router({ const orpcHandler = new ORPCHandler(appRouter) -export const orpcClient = createORPCFetchClient({ - baseURL: 'http://localhost:3000', +const orpcLink = new ORPCLink({ + url: 'http://localhost:3000', - async fetch(...args) { + async fetch(input, init) { await new Promise(resolve => setTimeout(resolve, 100)) - const request = new Request(...args) + const request = new Request(input, init) return orpcHandler.fetch(request) }, }) +export const orpcClient = createORPCClient(orpcLink) + export const orpc = createORPCReactQueryUtils(orpcClient) export const queryClient = new QueryClient({ diff --git a/packages/react/src/procedure-utils.ts b/packages/react/src/procedure-utils.ts index e27c7114..b9466389 100644 --- a/packages/react/src/procedure-utils.ts +++ b/packages/react/src/procedure-utils.ts @@ -154,7 +154,7 @@ export interface ProcedureUtils { } export interface CreateProcedureUtilsOptions { - client: ProcedureClient + client: ProcedureClient queryClient: QueryClient /** diff --git a/packages/react/src/react-context.ts b/packages/react/src/react-context.ts index b895fab6..feed530c 100644 --- a/packages/react/src/react-context.ts +++ b/packages/react/src/react-context.ts @@ -2,20 +2,20 @@ import type { RouterClient } from '@orpc/server' import type { QueryClient } from '@tanstack/react-query' import { type Context, createContext, useContext } from 'react' -export interface ORPCContextValue> { +export interface ORPCContextValue> { client: T queryClient: QueryClient } -export type ORPCContext> = Context< +export type ORPCContext> = Context< ORPCContextValue | undefined > -export function createORPCContext>(): ORPCContext { +export function createORPCContext>(): ORPCContext { return createContext(undefined as any) } -export function useORPCContext>( +export function useORPCContext>( context: ORPCContext, ): ORPCContextValue { const value = useContext(context) diff --git a/packages/react/src/react-hooks.ts b/packages/react/src/react-hooks.ts index 630ddbd6..8bf40139 100644 --- a/packages/react/src/react-hooks.ts +++ b/packages/react/src/react-hooks.ts @@ -4,14 +4,14 @@ import { createGeneralHooks, type GeneralHooks } from './general-hooks' import { orpcPathSymbol } from './orpc-path' import { createProcedureHooks, type ProcedureHooks } from './procedure-hooks' -export type ORPCHooks> = - T extends ProcedureClient +export type ORPCHooks> = + T extends ProcedureClient ? ProcedureHooks & GeneralHooks : { - [K in keyof T]: T[K] extends RouterClient ? ORPCHooks : never + [K in keyof T]: T[K] extends RouterClient ? ORPCHooks : never } & GeneralHooks -export interface CreateORPCHooksOptions> { +export interface CreateORPCHooksOptions> { context: ORPCContext /** @@ -22,7 +22,7 @@ export interface CreateORPCHooksOptions> { path?: string[] } -export function createORPCHooks>( +export function createORPCHooks>( options: CreateORPCHooksOptions, ): ORPCHooks { const path = options.path ?? [] diff --git a/packages/react/src/react-utils.ts b/packages/react/src/react-utils.ts index ebf36ed7..5b0f11bf 100644 --- a/packages/react/src/react-utils.ts +++ b/packages/react/src/react-utils.ts @@ -3,14 +3,14 @@ import type { ORPCContextValue } from './react-context' import { createGeneralUtils, type GeneralUtils } from './general-utils' import { createProcedureUtils, type ProcedureUtils } from './procedure-utils' -export type ORPCUtils> = - T extends ProcedureClient +export type ORPCUtils> = + T extends ProcedureClient ? ProcedureUtils & GeneralUtils : { - [K in keyof T]: T[K] extends RouterClient ? ORPCUtils : never + [K in keyof T]: T[K] extends RouterClient ? ORPCUtils : never } & GeneralUtils -export interface CreateORPCUtilsOptions> { +export interface CreateORPCUtilsOptions> { contextValue: ORPCContextValue /** @@ -21,7 +21,7 @@ export interface CreateORPCUtilsOptions> { path?: string[] } -export function createORPCUtils>( +export function createORPCUtils>( options: CreateORPCUtilsOptions, ): ORPCUtils { const path = options.path ?? [] diff --git a/packages/react/src/react.tsx b/packages/react/src/react.tsx index 8efdc08d..7a94cec2 100644 --- a/packages/react/src/react.tsx +++ b/packages/react/src/react.tsx @@ -8,14 +8,14 @@ import { createORPCHooks } from './react-hooks' import { createORPCUtils } from './react-utils' import { useQueriesFactory } from './use-queries/hook' -export type ORPCReact> = +export type ORPCReact> = ORPCHooks & { useContext: () => ORPCContextValue useUtils: () => ORPCUtils useQueries: UseQueries } -export function createORPCReact>(): { +export function createORPCReact>(): { orpc: ORPCReact ORPCContext: ORPCContext } { diff --git a/packages/react/src/use-queries/builder.test.ts b/packages/react/src/use-queries/builder.test.ts index 769d41da..a9f00898 100644 --- a/packages/react/src/use-queries/builder.test.ts +++ b/packages/react/src/use-queries/builder.test.ts @@ -16,7 +16,7 @@ it('createUseQueriesBuilder', async () => { expect(options.queryFn).toBeInstanceOf(Function) - const result = await (options as any).queryFn({} as any) + const result = await (options as any).queryFn({}) expect(result).toEqual({ id: '123', diff --git a/packages/react/src/use-queries/builder.ts b/packages/react/src/use-queries/builder.ts index 46b59c28..faa0eedf 100644 --- a/packages/react/src/use-queries/builder.ts +++ b/packages/react/src/use-queries/builder.ts @@ -32,7 +32,7 @@ export interface UseQueriesBuilder { } export interface CreateUseQueriesBuilderOptions { - client: ProcedureClient + client: ProcedureClient /** * The path of procedure on server diff --git a/packages/react/src/use-queries/builders.ts b/packages/react/src/use-queries/builders.ts index afed01a7..bb7cf5f9 100644 --- a/packages/react/src/use-queries/builders.ts +++ b/packages/react/src/use-queries/builders.ts @@ -2,14 +2,14 @@ import type { ProcedureClient, RouterClient } from '@orpc/server' import type {} from '@tanstack/react-query' import { createUseQueriesBuilder, type UseQueriesBuilder } from './builder' -export type UseQueriesBuilders> = - T extends ProcedureClient +export type UseQueriesBuilders> = + T extends ProcedureClient ? UseQueriesBuilder : { - [K in keyof T]: T[K] extends RouterClient ? UseQueriesBuilders : never + [K in keyof T]: T[K] extends RouterClient ? UseQueriesBuilders : never } -export interface CreateUseQueriesBuildersOptions> { +export interface CreateUseQueriesBuildersOptions> { client: T /** @@ -18,7 +18,7 @@ export interface CreateUseQueriesBuildersOptions> { path?: string[] } -export function createUseQueriesBuilders>( +export function createUseQueriesBuilders>( options: CreateUseQueriesBuildersOptions, ): UseQueriesBuilders { const path = options.path ?? [] diff --git a/packages/react/src/use-queries/hook.ts b/packages/react/src/use-queries/hook.ts index 07899e12..0b1266f5 100644 --- a/packages/react/src/use-queries/hook.ts +++ b/packages/react/src/use-queries/hook.ts @@ -9,7 +9,7 @@ import { useQueries } from '@tanstack/react-query' import { useORPCContext } from '../react-context' import { createUseQueriesBuilders } from './builders' -export interface UseQueries> { +export interface UseQueries> { = [], UCombinedResult = QueriesResults>( build: ( builders: UseQueriesBuilders, @@ -18,11 +18,11 @@ export interface UseQueries> { ): UCombinedResult } -export interface UseQueriesFactoryOptions> { +export interface UseQueriesFactoryOptions> { context: ORPCContext } -export function useQueriesFactory>( +export function useQueriesFactory>( options: UseQueriesFactoryOptions, ): UseQueries { const Hook = (build: any, combine?: any): any => { diff --git a/packages/react/tests/orpc.tsx b/packages/react/tests/orpc.tsx index 5f7501a7..d4e76892 100644 --- a/packages/react/tests/orpc.tsx +++ b/packages/react/tests/orpc.tsx @@ -1,5 +1,6 @@ import type { RouterClient } from '@orpc/server' -import { createORPCFetchClient } from '@orpc/client' +import { createORPCClient } from '@orpc/client' +import { ORPCLink } from '@orpc/client/fetch' import { os } from '@orpc/server' import { ORPCHandler } from '@orpc/server/fetch' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' @@ -95,18 +96,19 @@ export const appRouter = orpcServer.router({ const orpcHandler = new ORPCHandler(appRouter) -export const orpcClient = createORPCFetchClient({ - baseURL: 'http://localhost:3000', - - async fetch(...args) { +const orpcLink = new ORPCLink({ + url: 'http://localhost:3000', + async fetch(input, init) { await new Promise(resolve => setTimeout(resolve, 100)) - const request = new Request(...args) + const request = new Request(input, init) return orpcHandler.fetch(request) }, }) -export const { orpc, ORPCContext } = createORPCReact>() +export const orpcClient = createORPCClient(orpcLink) + +export const { orpc, ORPCContext } = createORPCReact>() export const queryClient = new QueryClient({ defaultOptions: { diff --git a/packages/server/src/lazy-decorated.test-d.ts b/packages/server/src/lazy-decorated.test-d.ts index 6e30a7ea..a45985bf 100644 --- a/packages/server/src/lazy-decorated.test-d.ts +++ b/packages/server/src/lazy-decorated.test-d.ts @@ -42,7 +42,7 @@ describe('DecoratedLazy', () => { expectTypeOf(decorated).toMatchTypeOf>() expectTypeOf(decorated).toMatchTypeOf< - ProcedureClient + ProcedureClient >() }) @@ -53,19 +53,19 @@ describe('DecoratedLazy', () => { expectTypeOf({ router: decorated }).toMatchTypeOf() expectTypeOf(decorated.ping).toMatchTypeOf>() - expectTypeOf(decorated.ping).toMatchTypeOf >() + expectTypeOf(decorated.ping).toMatchTypeOf>() expectTypeOf(decorated.pong).toMatchTypeOf>() - expectTypeOf(decorated.pong).toMatchTypeOf>() + expectTypeOf(decorated.pong).toMatchTypeOf>() expectTypeOf(decorated.nested).toMatchTypeOf>() expectTypeOf({ router: decorated.nested }).toMatchTypeOf() expectTypeOf(decorated.nested.ping).toMatchTypeOf>() - expectTypeOf(decorated.nested.ping).toMatchTypeOf>() + expectTypeOf(decorated.nested.ping).toMatchTypeOf>() expectTypeOf(decorated.nested.pong).toMatchTypeOf>() - expectTypeOf(decorated.nested.pong).toMatchTypeOf>() + expectTypeOf(decorated.nested.pong).toMatchTypeOf>() }) it('flat lazy', () => { diff --git a/packages/server/src/lazy-decorated.ts b/packages/server/src/lazy-decorated.ts index a258de51..ef68ba19 100644 --- a/packages/server/src/lazy-decorated.ts +++ b/packages/server/src/lazy-decorated.ts @@ -13,7 +13,7 @@ export type DecoratedLazy = T extends Lazy & ( T extends Procedure ? undefined extends UContext - ? ProcedureClient, SchemaOutput> + ? ProcedureClient, SchemaOutput, unknown> : unknown : { [K in keyof T]: T[K] extends object ? DecoratedLazy : never diff --git a/packages/server/src/procedure-client.test-d.ts b/packages/server/src/procedure-client.test-d.ts index 272a9a71..e713179f 100644 --- a/packages/server/src/procedure-client.test-d.ts +++ b/packages/server/src/procedure-client.test-d.ts @@ -10,23 +10,23 @@ beforeEach(() => { }) describe('ProcedureClient', () => { - const fn: ProcedureClient = async (input, options) => { + const fn: ProcedureClient = async (...[input, options]) => { expectTypeOf(input).toEqualTypeOf() - expectTypeOf(options).toEqualTypeOf() + expectTypeOf(options).toEqualTypeOf<(WithSignal & { context?: unknown }) | undefined>() return 123 } - const fnWithOptionalInput: ProcedureClient = async (...args) => { + const fnWithOptionalInput: ProcedureClient = async (...args) => { const [input, options] = args expectTypeOf(input).toEqualTypeOf() - expectTypeOf(options).toEqualTypeOf() + expectTypeOf(options).toEqualTypeOf<(WithSignal & { context?: unknown }) | undefined>() return 123 } it('just a function', () => { - expectTypeOf(fn).toEqualTypeOf<(input: string, options?: WithSignal) => Promise>() - expectTypeOf(fnWithOptionalInput).toMatchTypeOf<(input: string | undefined, options?: WithSignal) => Promise>() + expectTypeOf(fn).toMatchTypeOf<(input: string, options: WithSignal & { context?: unknown }) => Promise>() + expectTypeOf(fnWithOptionalInput).toMatchTypeOf<(input: string | undefined, options: WithSignal & { context?: unknown }) => Promise>() }) it('infer correct input', () => { @@ -59,6 +59,36 @@ describe('ProcedureClient', () => { // @ts-expect-error - input is required expectTypeOf(fn()).toEqualTypeOf>() }) + + describe('context', () => { + it('can accept context', () => { + const client = {} as ProcedureClient<{ val: string }, { val: number }, { userId: string }> + + client({ val: '123' }, { context: { userId: '123' } }) + // @ts-expect-error - invalid context + client({ val: '123' }, { context: { userId: 123 } }) + // @ts-expect-error - context is required + client({ val: '123' }) + }) + + it('optional options when context is optional', () => { + const client = {} as ProcedureClient<{ val: string }, { val: number }, undefined | { userId: string }> + + client({ val: '123' }) + client({ val: '123' }, { context: { userId: '123' } }) + }) + + it('can call without args when both input and context are optional', () => { + const client = {} as ProcedureClient + + client() + client({ val: 'string' }, { context: { userId: '123' } }) + // @ts-expect-error - input is invalid + client({ val: 123 }, { context: { userId: '123' } }) + // @ts-expect-error - context is invalid + client({ val: '123' }, { context: { userId: 123 } }) + }) + }) }) describe('createProcedureClient', () => { @@ -71,7 +101,7 @@ describe('createProcedureClient', () => { procedure, }) - expectTypeOf(client).toEqualTypeOf>() + expectTypeOf(client).toEqualTypeOf>() }) it('context can be optional and can be a sync or async function', () => { @@ -192,5 +222,5 @@ it('support lazy procedure', () => { }, }) - expectTypeOf(client).toEqualTypeOf>() + expectTypeOf(client).toEqualTypeOf>() }) diff --git a/packages/server/src/procedure-client.ts b/packages/server/src/procedure-client.ts index c1abef5c..392b7e90 100644 --- a/packages/server/src/procedure-client.ts +++ b/packages/server/src/procedure-client.ts @@ -9,8 +9,17 @@ import { ORPCError } from '@orpc/shared/error' import { unlazy } from './lazy' import { mergeContext } from './utils' -export interface ProcedureClient { - (...opts: [input: TInput, options?: WithSignal] | (undefined extends TInput ? [] : never)): Promise +export type ProcedureClientOptions = + & WithSignal + & (undefined extends TClientContext ? { context?: TClientContext } : { context: TClientContext }) + +export interface ProcedureClient { + ( + ...opts: + | [input: TInput, options: ProcedureClientOptions] + | (undefined extends TInput & TClientContext ? [] : never) + | (undefined extends TClientContext ? [input: TInput] : never) + ): Promise } /** @@ -47,7 +56,7 @@ export function createProcedureClient< TFuncOutput extends SchemaInput = SchemaInput, >( options: CreateProcedureClientOptions, -): ProcedureClient, SchemaOutput> { +): ProcedureClient, SchemaOutput, unknown> { return async (...[input, callerOptions]) => { const path = options.path ?? [] const { default: procedure } = await unlazy(options.procedure) diff --git a/packages/server/src/procedure-decorated.ts b/packages/server/src/procedure-decorated.ts index 48cc97c7..89a21734 100644 --- a/packages/server/src/procedure-decorated.ts +++ b/packages/server/src/procedure-decorated.ts @@ -72,7 +72,7 @@ export type DecoratedProcedure< ) => DecoratedProcedure } - & (undefined extends TContext ? ProcedureClient, SchemaOutput> : unknown) + & (undefined extends TContext ? ProcedureClient, SchemaOutput, unknown> : unknown) export function decorateProcedure< TContext extends Context, diff --git a/packages/server/src/router-client.test-d.ts b/packages/server/src/router-client.test-d.ts index 056f5da0..6b504312 100644 --- a/packages/server/src/router-client.test-d.ts +++ b/packages/server/src/router-client.test-d.ts @@ -29,29 +29,29 @@ const routerWithLazy = { describe('RouterClient', () => { it('router without lazy', () => { - const client = {} as RouterClient + const client = {} as RouterClient expectTypeOf(client.ping).toEqualTypeOf< - ProcedureClient<{ val: string }, { val: number }> + ProcedureClient<{ val: string }, { val: number }, unknown> >() expectTypeOf(client.pong).toEqualTypeOf< - ProcedureClient + ProcedureClient >() expectTypeOf(client.nested.ping).toEqualTypeOf< - ProcedureClient<{ val: string }, { val: number }> + ProcedureClient<{ val: string }, { val: number }, unknown> >() expectTypeOf(client.nested.pong).toEqualTypeOf< - ProcedureClient + ProcedureClient >() }) it('support lazy', () => { - expectTypeOf>().toEqualTypeOf>() + expectTypeOf>().toEqualTypeOf>() }) it('support procedure as router', () => { - expectTypeOf>().toEqualTypeOf>() + expectTypeOf>().toEqualTypeOf>() }) }) @@ -62,13 +62,13 @@ describe('createRouterClient', () => { context: { auth: true }, }) - expectTypeOf(client).toMatchTypeOf>() + expectTypeOf(client).toMatchTypeOf>() const client2 = createRouterClient({ router: routerWithLazy, context: { auth: true }, }) - expectTypeOf(client2).toMatchTypeOf>() + expectTypeOf(client2).toMatchTypeOf>() }) it('required context when needed', () => { diff --git a/packages/server/src/router-client.ts b/packages/server/src/router-client.ts index 4de9ba3f..b73a6f3e 100644 --- a/packages/server/src/router-client.ts +++ b/packages/server/src/router-client.ts @@ -10,15 +10,15 @@ import { isProcedure } from './procedure' import { createProcedureClient } from './procedure-client' import { type ANY_ROUTER, getRouterChild, type Router } from './router' -export type RouterClient = -T extends Lazy - ? RouterClient - : T extends +export type RouterClient = +TRouter extends Lazy + ? RouterClient + : TRouter extends | ContractProcedure | Procedure - ? ProcedureClient, SchemaOutput> + ? ProcedureClient, SchemaOutput, TClientContext> : { - [K in keyof T]: T[K] extends ANY_ROUTER | ContractRouter ? RouterClient : never + [K in keyof TRouter]: TRouter[K] extends ANY_ROUTER | ContractRouter ? RouterClient : never } export type CreateRouterClientOptions< @@ -43,7 +43,7 @@ export function createRouterClient< TRouter extends ANY_ROUTER, >( options: CreateRouterClientOptions, -): RouterClient { +): RouterClient { if (isProcedure(options.router)) { const caller = createProcedureClient({ ...options, diff --git a/packages/vue-query/src/types.ts b/packages/vue-query/src/types.ts index eb964bbb..adc4477d 100644 --- a/packages/vue-query/src/types.ts +++ b/packages/vue-query/src/types.ts @@ -16,8 +16,9 @@ export type NonUndefinedGuard = T extends undefined ? never : T export type InferCursor = T extends { cursor?: any } ? T['cursor'] : never -export type QueryOptions = +export type QueryOptions = & (undefined extends TInput ? { input?: MaybeDeepRef } : { input: MaybeDeepRef }) + & (undefined extends TClientContext ? { context?: MaybeDeepRef } : { context: MaybeDeepRef }) & SetOptional<{ [P in keyof QueryObserverOptions]: P extends 'enabled' ? MaybeRefOrGetter[P]> @@ -28,14 +29,16 @@ export type QueryOptions = initialData?: NonUndefinedGuard | (() => NonUndefinedGuard) | undefined } -export type InfiniteOptions = +export type InfiniteOptions = & (undefined extends TInput ? { input?: MaybeDeepRef> } : { input: MaybeDeepRef> }) + & (undefined extends TClientContext ? { context?: MaybeDeepRef } : { context: MaybeDeepRef }) & SetOptional< UseInfiniteQueryOptions>, 'queryKey' | (undefined extends InferCursor ? 'initialPageParam' : never) > -export type MutationOptions = +export type MutationOptions = + & (undefined extends TClientContext ? { context?: MaybeDeepRef } : { context: MaybeDeepRef }) & { [P in keyof MutationObserverOptions]: MaybeDeepRef[P]> } diff --git a/packages/vue-query/src/utils-procedure.test-d.ts b/packages/vue-query/src/utils-procedure.test-d.ts index 7e84f549..aedfdceb 100644 --- a/packages/vue-query/src/utils-procedure.test-d.ts +++ b/packages/vue-query/src/utils-procedure.test-d.ts @@ -1,16 +1,17 @@ import type { ProcedureClient } from '@orpc/server' import type { InfiniteData, QueryKey } from '@tanstack/vue-query' -import { useInfiniteQuery, useQuery } from '@tanstack/vue-query' +import type { ProcedureUtils } from './utils-procedure' +import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/vue-query' import { ref } from 'vue' import { createProcedureUtils } from './utils-procedure' describe('queryOptions', () => { - const client = vi.fn >( + const client = vi.fn >( (...[input]) => Promise.resolve(input?.toString()), ) const utils = createProcedureUtils(client, []) - const client2 = vi.fn((input: number) => Promise.resolve(input.toString())) + const client2 = {} as ProcedureClient const utils2 = createProcedureUtils(client2, []) it('infer correct input type', () => { @@ -52,13 +53,42 @@ describe('queryOptions', () => { expectTypeOf(query.data.value).toEqualTypeOf<{ value: number } | undefined>() }) + + describe('client context', () => { + it('can be optional', () => { + const utils = {} as ProcedureUtils + useQuery(utils.queryOptions()) + useQuery(utils.queryOptions({})) + useQuery(utils.queryOptions({ context: undefined })) + useQuery(utils.queryOptions({ context: { batch: true } })) + useQuery(utils.queryOptions({ context: { batch: ref(true) } })) + // @ts-expect-error --- invalid context + useQuery(utils.queryOptions({ context: { batch: 'invalid' } })) + // @ts-expect-error --- invalid context + useQuery(utils.queryOptions({ context: { batch: ref('invalid') } })) + }) + + it('required pass context when non-optional', () => { + const utils = {} as ProcedureUtils + // @ts-expect-error --- missing context + useQuery(utils.queryOptions()) + // @ts-expect-error --- missing context + useQuery(utils.queryOptions({})) + useQuery(utils.queryOptions({ context: { batch: true } })) + useQuery(utils.queryOptions({ context: { batch: ref(false) } })) + // @ts-expect-error --- invalid context + useQuery(utils.queryOptions({ context: { batch: 'invalid' } })) + // @ts-expect-error --- invalid context + useQuery(utils.queryOptions({ context: { batch: ref('invalid') } })) + }) + }) }) describe('infiniteOptions', () => { const getNextPageParam = vi.fn() it('cannot use on procedure without input object-able', () => { - const utils = createProcedureUtils({} as (input: number) => Promise, []) + const utils = createProcedureUtils({} as ProcedureClient, []) // @ts-expect-error missing initialPageParam utils.infiniteOptions({ @@ -81,7 +111,7 @@ describe('infiniteOptions', () => { }) it('infer correct input type', () => { - const utils = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, []) + const utils = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor: number }, string, undefined>, []) utils.infiniteOptions({ input: { @@ -126,7 +156,7 @@ describe('infiniteOptions', () => { }) it('infer correct initialPageParam type', () => { - const utils = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, []) + const utils = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor: number }, string, undefined>, []) utils.infiniteOptions({ input: {}, @@ -162,14 +192,14 @@ describe('infiniteOptions', () => { }) it('initialPageParam can be optional', () => { - const utils = createProcedureUtils({} as (input: { limit?: number, cursor?: number }) => Promise, []) + const utils = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor?: number }, string, undefined>, []) utils.infiniteOptions({ input: {}, getNextPageParam, }) - const utils2 = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, []) + const utils2 = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor: number }, string, undefined>, []) // @ts-expect-error initialPageParam is required utils2.infiniteOptions({ @@ -179,13 +209,13 @@ describe('infiniteOptions', () => { }) it('input can be optional', () => { - const utils = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor?: number } | undefined, string>, []) + const utils = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor?: number } | undefined, string, undefined>, []) utils.infiniteOptions({ getNextPageParam, }) - const utils2 = createProcedureUtils({} as (input: { limit?: number, cursor?: number }) => Promise, []) + const utils2 = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor?: number }, string, undefined>, []) // @ts-expect-error input is required utils2.infiniteOptions({ @@ -194,7 +224,7 @@ describe('infiniteOptions', () => { }) it('infer correct output type', () => { - const utils = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, []) + const utils = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor: number }, string, undefined>, []) const query = useInfiniteQuery(utils.infiniteOptions({ input: { limit: 1, @@ -207,7 +237,7 @@ describe('infiniteOptions', () => { }) it('work with select options', () => { - const utils = createProcedureUtils({} as (input: { limit?: number, cursor: number }) => Promise, []) + const utils = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor: number }, string, undefined>, []) const query = useInfiniteQuery(utils.infiniteOptions({ input: { limit: ref(1), @@ -223,10 +253,44 @@ describe('infiniteOptions', () => { expectTypeOf(query.data.value).toEqualTypeOf<{ value: number } | undefined>() }) + + describe('client context', () => { + it('can be optional', () => { + const utils = {} as ProcedureUtils + + const getNextPageParam = vi.fn() + const initialPageParam = 1 + + useInfiniteQuery(utils.infiniteOptions({ getNextPageParam, initialPageParam })) + useInfiniteQuery(utils.infiniteOptions({ getNextPageParam, initialPageParam, context: undefined })) + useInfiniteQuery(utils.infiniteOptions({ getNextPageParam, initialPageParam, context: { batch: true } })) + useInfiniteQuery(utils.infiniteOptions({ getNextPageParam, initialPageParam, context: { batch: ref(false) } })) + // @ts-expect-error --- invalid context + useInfiniteQuery(utils.infiniteOptions({ getNextPageParam, initialPageParam, context: { batch: 'invalid' } })) + // @ts-expect-error --- invalid context + useInfiniteQuery(utils.infiniteOptions({ getNextPageParam, initialPageParam, context: { batch: ref('invalid') } })) + }) + + it('required pass context when non-optional', () => { + const utils = {} as ProcedureUtils + + const getNextPageParam = vi.fn() + const initialPageParam = 1 + + // @ts-expect-error --- missing context + useInfiniteQuery(utils.infiniteOptions({ getNextPageParam, initialPageParam })) + useInfiniteQuery(utils.infiniteOptions({ getNextPageParam, initialPageParam, context: { batch: true } })) + useInfiniteQuery(utils.infiniteOptions({ getNextPageParam, initialPageParam, context: { batch: ref(false) } })) + // @ts-expect-error --- invalid context + useInfiniteQuery(utils.infiniteOptions({ getNextPageParam, initialPageParam, context: { batch: 'invalid' } })) + // @ts-expect-error --- invalid context + useInfiniteQuery(utils.infiniteOptions({ getNextPageParam, initialPageParam, context: { batch: ref('invalid') } })) + }) + }) }) describe('mutationOptions', () => { - const client = vi.fn((input: number) => Promise.resolve(input.toString())) + const client = {} as ProcedureClient const utils = createProcedureUtils(client, []) it('infer correct input type', () => { @@ -261,4 +325,33 @@ describe('mutationOptions', () => { expectTypeOf(option.mutationKey).toEqualTypeOf() expectTypeOf(option.mutationFn).toMatchTypeOf<(input: number) => Promise>() }) + + describe('client context', () => { + it('can be optional', () => { + const utils = {} as ProcedureUtils + useMutation(utils.mutationOptions()) + useMutation(utils.mutationOptions({})) + useMutation(utils.mutationOptions({ context: undefined })) + useMutation(utils.mutationOptions({ context: { batch: true } })) + useMutation(utils.mutationOptions({ context: { batch: ref(false) } })) + // @ts-expect-error --- invalid context + useMutation(utils.mutationOptions({ context: { batch: 'invalid' } })) + // @ts-expect-error --- invalid context + useMutation(utils.mutationOptions({ context: { batch: ref('invalid') } })) + }) + + it('required pass context when non-optional', () => { + const utils = {} as ProcedureUtils + // @ts-expect-error --- missing context + useMutation(utils.mutationOptions()) + // @ts-expect-error --- missing context + useMutation(utils.mutationOptions({})) + useMutation(utils.mutationOptions({ context: { batch: true } })) + useMutation(utils.mutationOptions({ context: { batch: ref(false) } })) + // @ts-expect-error --- invalid context + useMutation(utils.mutationOptions({ context: { batch: 123 } })) + // @ts-expect-error --- invalid context + useMutation(utils.mutationOptions({ context: { batch: ref(123) } })) + }) + }) }) diff --git a/packages/vue-query/src/utils-procedure.test.ts b/packages/vue-query/src/utils-procedure.test.ts index f316a229..8ec760e4 100644 --- a/packages/vue-query/src/utils-procedure.test.ts +++ b/packages/vue-query/src/utils-procedure.test.ts @@ -1,4 +1,3 @@ -import type { ProcedureClient } from '@orpc/server' import { ref } from 'vue' import * as keyModule from './key' import { createProcedureUtils } from './utils-procedure' @@ -13,7 +12,7 @@ beforeEach(() => { }) describe('queryOptions', () => { - const client = vi.fn>( + const client = vi.fn( (...[input]) => Promise.resolve(input?.toString()), ) const utils = createProcedureUtils(client, ['ping']) @@ -48,13 +47,29 @@ describe('queryOptions', () => { expect(client).toHaveBeenCalledTimes(1) expect(client).toBeCalledWith(1, { signal }) }) + + it('works with client context', async () => { + const client = vi.fn((...[input]) => Promise.resolve(input?.toString())) + const utils = createProcedureUtils(client, ['ping']) + + const options = utils.queryOptions({ context: { batch: ref(true) } }) + + expect(options.queryKey.value).toEqual(['__ORPC__', ['ping'], { type: 'query' }]) + expect(buildKeySpy).toHaveBeenCalledTimes(1) + expect(buildKeySpy).toHaveBeenCalledWith(['ping'], { type: 'query' }) + + client.mockResolvedValueOnce('__mocked__') + await expect((options as any).queryFn({ signal })).resolves.toEqual('__mocked__') + expect(client).toHaveBeenCalledTimes(1) + expect(client).toBeCalledWith(undefined, { signal, context: { batch: true } }) + }) }) describe('infiniteOptions', () => { const getNextPageParam = vi.fn() it('works ', async () => { - const client = vi.fn<(input: { limit?: number, cursor: number | undefined }) => Promise>() + const client = vi.fn() const utils = createProcedureUtils(client, []) const options = utils.infiniteOptions({ @@ -75,7 +90,7 @@ describe('infiniteOptions', () => { }) it('works without initialPageParam', async () => { - const client = vi.fn<(input: { limit?: number, cursor: number | undefined }) => Promise>() + const client = vi.fn() const utils = createProcedureUtils(client, []) const options = utils.infiniteOptions({ @@ -94,7 +109,7 @@ describe('infiniteOptions', () => { }) it('works with ref', async () => { - const client = vi.fn<(input: { limit?: number, cursor: number | undefined }) => Promise>() + const client = vi.fn() const utils = createProcedureUtils(client, []) const input = ref({ limit: ref(5) }) @@ -114,10 +129,30 @@ describe('infiniteOptions', () => { expect(client).toHaveBeenCalledTimes(1) expect(client).toBeCalledWith({ limit: 5, cursor: 1 }, { signal }) }) + + it('works with client context', async () => { + const client = vi.fn() + const utils = createProcedureUtils(client, []) + + const options = utils.infiniteOptions({ + context: { batch: ref(true) }, + getNextPageParam, + initialPageParam: 1, + }) + + expect(options.queryKey.value).toEqual(['__ORPC__', [], { type: 'infinite' }]) + expect(buildKeySpy).toHaveBeenCalledTimes(1) + expect(buildKeySpy).toHaveBeenCalledWith([], { type: 'infinite' }) + + client.mockResolvedValueOnce('__mocked__') + await expect((options as any).queryFn({ pageParam: 1, signal })).resolves.toEqual('__mocked__') + expect(client).toHaveBeenCalledTimes(1) + expect(client).toBeCalledWith({ limit: undefined, cursor: 1 }, { signal, context: { batch: true } }) + }) }) describe('mutationOptions', () => { - const client = vi.fn>( + const client = vi.fn( (...[input]) => Promise.resolve(input?.toString()), ) const utils = createProcedureUtils(client, ['ping']) @@ -136,6 +171,24 @@ describe('mutationOptions', () => { client.mockResolvedValueOnce('__mocked__') await expect(options.mutationFn(1)).resolves.toEqual('__mocked__') expect(client).toHaveBeenCalledTimes(1) - expect(client).toBeCalledWith(1) + expect(client).toBeCalledWith(1, {}) + }) + + it('works with client context', async () => { + const client = vi.fn( + (...[input]) => Promise.resolve(input?.toString()), + ) + const utils = createProcedureUtils(client, ['ping']) + + const options = utils.mutationOptions({ context: { batch: ref(true) } }) + + expect(options.mutationKey).toEqual(['__ORPC__', ['ping'], { type: 'mutation' }]) + expect(buildKeySpy).toHaveBeenCalledTimes(1) + expect(buildKeySpy).toHaveBeenCalledWith(['ping'], { type: 'mutation' }) + + client.mockResolvedValueOnce('__mocked__') + await expect(options.mutationFn(1)).resolves.toEqual('__mocked__') + expect(client).toHaveBeenCalledTimes(1) + expect(client).toBeCalledWith(1, { context: { batch: true } }) }) }) diff --git a/packages/vue-query/src/utils-procedure.ts b/packages/vue-query/src/utils-procedure.ts index 381c951f..63b1c032 100644 --- a/packages/vue-query/src/utils-procedure.ts +++ b/packages/vue-query/src/utils-procedure.ts @@ -10,35 +10,35 @@ import { deepUnref } from './utils' /** * Utils at procedure level */ -export interface ProcedureUtils { - queryOptions: >( - ...options: [U] | (undefined extends TInput ? [] : never) - ) => IsEqual> extends true +export interface ProcedureUtils { + queryOptions: >( + ...opt: [options: U] | (undefined extends TInput & TClientContext ? [] : never) + ) => IsEqual> extends true ? { queryKey: QueryKey, queryFn: () => Promise } : Omit<{ queryKey: ComputedRef, queryFn: () => Promise }, keyof U> & U - infiniteOptions: >( + infiniteOptions: >( options: U ) => Omit<{ queryKey: ComputedRef, queryFn: () => Promise, initialPageParam: undefined }, keyof U> & U - mutationOptions: >( - options?: U - ) => IsEqual> extends true + mutationOptions: >( + ...opt: [options: U] | (undefined extends TClientContext ? [] : never) + ) => IsEqual> extends true ? { mutationKey: QueryKey, mutationFn: (input: TInput) => Promise } : Omit<{ mutationKey: QueryKey, mutationFn: (input: TInput) => Promise }, keyof U> & U } -export function createProcedureUtils( - client: ProcedureClient, +export function createProcedureUtils( + client: ProcedureClient, path: string[], -): ProcedureUtils { +): ProcedureUtils { return { queryOptions(...[options]) { const input = options?.input as any return { queryKey: computed(() => buildKey(path, { type: 'query', input: deepUnref(input) })), - queryFn: ({ signal }) => client(deepUnref(input), { signal }), + queryFn: ({ signal }) => client(deepUnref(input), { signal, context: deepUnref(options?.context) } as any), ...(options as any), } }, @@ -48,15 +48,15 @@ export function createProcedureUtils( return { queryKey: computed(() => buildKey(path, { type: 'infinite', input: deepUnref(input) })), - queryFn: ({ pageParam, signal }) => client({ ...deepUnref(input), cursor: pageParam }, { signal }), + queryFn: ({ pageParam, signal }) => client({ ...deepUnref(input), cursor: pageParam }, { signal, context: deepUnref(options.context) } as any), ...(options as any), } }, - mutationOptions(options) { + mutationOptions(...[options]) { return { mutationKey: buildKey(path, { type: 'mutation' }), - mutationFn: input => client(input), + mutationFn: input => client(input, { context: deepUnref(options?.context) } as any), ...(options as any), } }, diff --git a/packages/vue-query/src/utils-router.test-d.ts b/packages/vue-query/src/utils-router.test-d.ts index 488ec9b7..be71ea49 100644 --- a/packages/vue-query/src/utils-router.test-d.ts +++ b/packages/vue-query/src/utils-router.test-d.ts @@ -1,4 +1,6 @@ import type { RouterClient } from '@orpc/server' +import type { GeneralUtils } from './utils-general' +import type { ProcedureUtils } from './utils-procedure' import { oc } from '@orpc/contract' import { os } from '@orpc/server' import { z } from 'zod' @@ -23,7 +25,7 @@ const router = os.contract(contractRouter).router({ describe('with contract router', () => { it('build correct types', () => { - const utils = createRouterUtils({} as RouterClient) + const utils = createRouterUtils({} as RouterClient) const generalUtils = createGeneralUtils([]) const pingUtils = createProcedureUtils(ping, []) @@ -41,7 +43,7 @@ describe('with contract router', () => { describe('with router', () => { it('build correct types', () => { - const utils = createRouterUtils({} as RouterClient) + const utils = createRouterUtils({} as RouterClient) const generalUtils = createGeneralUtils([]) const pingUtils = createProcedureUtils(ping, []) @@ -56,3 +58,19 @@ describe('with router', () => { expectTypeOf(utils.pong).toMatchTypeOf() }) }) + +it('with client context', () => { + const utils = createRouterUtils({} as RouterClient) + + const generalUtils = {} as GeneralUtils + const pingUtils = {} as ProcedureUtils<{ name: string }, string, undefined | { batch?: boolean }> + const pingGeneralUtils = createGeneralUtils<{ name: string }>(['ping']) + const pongUtils = {} as ProcedureUtils + const pongGeneralUtils = {} as GeneralUtils + + expectTypeOf(utils).toMatchTypeOf() + expectTypeOf(utils.ping).toMatchTypeOf() + expectTypeOf(utils.ping).toMatchTypeOf() + expectTypeOf(utils.pong).toMatchTypeOf() + expectTypeOf(utils.pong).toMatchTypeOf() +}) diff --git a/packages/vue-query/src/utils-router.ts b/packages/vue-query/src/utils-router.ts index 1df8d268..5b7cb6f5 100644 --- a/packages/vue-query/src/utils-router.ts +++ b/packages/vue-query/src/utils-router.ts @@ -2,18 +2,18 @@ import type { ProcedureClient, RouterClient } from '@orpc/server' import { createGeneralUtils, type GeneralUtils } from './utils-general' import { createProcedureUtils, type ProcedureUtils } from './utils-procedure' -export type RouterUtils> = - T extends ProcedureClient - ? ProcedureUtils & GeneralUtils +export type RouterUtils> = + T extends ProcedureClient + ? ProcedureUtils & GeneralUtils : { - [K in keyof T]: T[K] extends RouterClient ? RouterUtils : never + [K in keyof T]: T[K] extends RouterClient ? RouterUtils : never } & GeneralUtils /** * @param client - The client create form `@orpc/client` * @param path - The base path for query key */ -export function createRouterUtils>( +export function createRouterUtils>( client: T, path: string[] = [], ): RouterUtils { diff --git a/packages/vue-query/tests/e2e.test-d.ts b/packages/vue-query/tests/e2e.test-d.ts index c6f209b7..c4fa2061 100644 --- a/packages/vue-query/tests/e2e.test-d.ts +++ b/packages/vue-query/tests/e2e.test-d.ts @@ -34,6 +34,16 @@ describe('useQuery', () => { // @ts-expect-error input is invalid useQuery(orpc.user.find.queryOptions({ input: { id: 123 } })) }) + + it('infer types correctly with client context', async () => { + useQuery(orpc.user.find.queryOptions({ input: { id: '123' } })) + useQuery(orpc.user.find.queryOptions({ input: { id: '123' }, context: { batch: true } })) + useQuery(orpc.user.find.queryOptions({ input: { id: '123' }, context: { batch: ref(false) } })) + // @ts-expect-error --- invalid context + useQuery(orpc.user.find.queryOptions({ input: { id: '123' }, context: { batch: 'invalid' } })) + // @ts-expect-error --- invalid context + useQuery(orpc.user.find.queryOptions({ input: { id: '123' }, context: { batch: ref('invalid') } })) + }) }) describe('useInfiniteQuery', () => { @@ -102,6 +112,37 @@ describe('useInfiniteQuery', () => { getNextPageParam: {} as any, })) }) + + it('infer types correctly with client context', async () => { + useInfiniteQuery(orpc.user.list.infiniteOptions({ + input: { keyword: 'keyword' }, + getNextPageParam: {} as any, + })) + useInfiniteQuery(orpc.user.list.infiniteOptions({ + input: { keyword: 'keyword' }, + getNextPageParam: {} as any, + context: { batch: true }, + })) + useInfiniteQuery(orpc.user.list.infiniteOptions({ + input: { keyword: 'keyword' }, + getNextPageParam: {} as any, + context: { batch: ref(true) }, + })) + // @ts-expect-error --- invalid context + useInfiniteQuery(orpc.user.list.infiniteOptions({ + input: { keyword: 'keyword' }, + getNextPageParam: {} as any, + // @ts-expect-error --- invalid context + context: { batch: 'invalid' }, + })) + // @ts-expect-error --- invalid context + useInfiniteQuery(orpc.user.list.infiniteOptions({ + input: { keyword: 'keyword' }, + getNextPageParam: {} as any, + // @ts-expect-error --- invalid context + context: { batch: ref('invalid') }, + })) + }) }) describe('useMutation', () => { @@ -116,6 +157,16 @@ describe('useMutation', () => { expectTypeOf(query.mutateAsync).toMatchTypeOf<(input: { id: string }) => Promise<{ id: string, name: string }>>() }) + + it('infer types correctly with client context', async () => { + useMutation(orpc.user.find.mutationOptions(({}))) + useMutation(orpc.user.find.mutationOptions(({ context: { batch: true } }))) + useMutation(orpc.user.find.mutationOptions(({ context: { batch: ref(false) } }))) + // @ts-expect-error --- invalid context + useMutation(orpc.user.find.mutationOptions(({ context: { batch: 'invalid' } }))) + // @ts-expect-error --- invalid context + useMutation(orpc.user.find.mutationOptions(({ context: { batch: ref('invalid') } }))) + }) }) describe('useQueries', () => { @@ -191,4 +242,32 @@ describe('useQueries', () => { ], }) }) + + it('infer types correctly with client context', async () => { + useQueries({ + queries: [ + orpc.user.find.queryOptions({ + input: { id: '0' }, + }), + orpc.user.find.queryOptions({ + input: { id: '0' }, + context: { batch: true }, + }), + orpc.user.find.queryOptions({ + input: { id: '0' }, + context: { batch: ref(false) }, + }), + orpc.user.find.queryOptions({ + input: { id: '0' }, + // @ts-expect-error --- invalid context + context: { batch: 'invalid' }, + }), + orpc.user.find.queryOptions({ + input: { id: '0' }, + // @ts-expect-error --- invalid context + context: { batch: ref('invalid') }, + }), + ], + }) + }) }) diff --git a/packages/vue-query/tests/helpers.ts b/packages/vue-query/tests/helpers.ts index 7374e4e2..381a177d 100644 --- a/packages/vue-query/tests/helpers.ts +++ b/packages/vue-query/tests/helpers.ts @@ -1,4 +1,5 @@ -import { createORPCFetchClient } from '@orpc/client' +import { createORPCClient } from '@orpc/client' +import { ORPCLink } from '@orpc/client/fetch' import { os } from '@orpc/server' import { ORPCHandler } from '@orpc/server/fetch' import { QueryClient } from '@tanstack/vue-query' @@ -87,16 +88,18 @@ export const appRouter = orpcServer.router({ const orpcHandler = new ORPCHandler(appRouter) -export const orpcClient = createORPCFetchClient({ - baseURL: 'http://localhost:3000', +const orpcLink = new ORPCLink({ + url: 'http://localhost:3000', - async fetch(...args) { + async fetch(input, init) { await new Promise(resolve => setTimeout(resolve, 100)) - const request = new Request(...args) + const request = new Request(input, init) return orpcHandler.fetch(request) }, }) +export const orpcClient = createORPCClient(orpcLink) + export const orpc = createORPCVueQueryUtils(orpcClient) export const queryClient = new QueryClient({ diff --git a/playgrounds/contract-openapi/src/playground-client.ts b/playgrounds/contract-openapi/src/playground-client.ts index 783b94bf..74c1f49d 100644 --- a/playgrounds/contract-openapi/src/playground-client.ts +++ b/playgrounds/contract-openapi/src/playground-client.ts @@ -3,12 +3,15 @@ */ import type { contract } from './contract' -import { createORPCFetchClient } from '@orpc/client' +import { createORPCClient } from '@orpc/client' +import { ORPCLink } from '@orpc/client/fetch' -const orpc = createORPCFetchClient({ - baseURL: 'http://localhost:3000/api', +const orpcLink = new ORPCLink({ + url: 'http://localhost:3000/api', }) +const orpc = createORPCClient(orpcLink) + const planets = await orpc.planet.list({}) const planet = await orpc.planet.find({ id: 1 }) diff --git a/playgrounds/contract-openapi/src/playground-react.ts b/playgrounds/contract-openapi/src/playground-react.ts index 695bb07a..6adebf5f 100644 --- a/playgrounds/contract-openapi/src/playground-react.ts +++ b/playgrounds/contract-openapi/src/playground-react.ts @@ -6,7 +6,7 @@ import type { RouterClient } from '@orpc/server' import type { contract } from './contract' import { createORPCReact } from '@orpc/react' -const { orpc } = createORPCReact>() +const { orpc } = createORPCReact>() const listQuery = orpc.planet.list.useInfiniteQuery({ input: {}, diff --git a/playgrounds/expressjs/src/playground-client.ts b/playgrounds/expressjs/src/playground-client.ts index b53f16ce..e2351549 100644 --- a/playgrounds/expressjs/src/playground-client.ts +++ b/playgrounds/expressjs/src/playground-client.ts @@ -3,12 +3,15 @@ */ import type { router } from './router' -import { createORPCFetchClient } from '@orpc/client' +import { createORPCClient } from '@orpc/client' +import { ORPCLink } from '@orpc/client/fetch' -const orpc = createORPCFetchClient({ - baseURL: 'http://localhost:3000/api', +const orpcLink = new ORPCLink({ + url: 'http://localhost:3000/api', }) +const orpc = createORPCClient(orpcLink) + const planets = await orpc.planet.list({}) const planet = await orpc.planet.find({ id: 1 }) diff --git a/playgrounds/expressjs/src/playground-react.ts b/playgrounds/expressjs/src/playground-react.ts index 708f65b8..e68fc44e 100644 --- a/playgrounds/expressjs/src/playground-react.ts +++ b/playgrounds/expressjs/src/playground-react.ts @@ -6,7 +6,7 @@ import type { RouterClient } from '@orpc/server' import type { router } from './router' import { createORPCReact } from '@orpc/react' -const { orpc } = createORPCReact >() +const { orpc } = createORPCReact>() const listQuery = orpc.planet.list.useInfiniteQuery({ input: {}, diff --git a/playgrounds/nextjs/src/lib/orpc.ts b/playgrounds/nextjs/src/lib/orpc.ts index 52a058ef..18e78bfb 100644 --- a/playgrounds/nextjs/src/lib/orpc.ts +++ b/playgrounds/nextjs/src/lib/orpc.ts @@ -1,12 +1,15 @@ import type { router } from '@/app/api/[...rest]/router' -import { createORPCFetchClient } from '@orpc/client' +import { createORPCClient } from '@orpc/client' +import { ORPCLink } from '@orpc/client/fetch' import { createORPCReact } from '@orpc/react' -export const orpcClient = createORPCFetchClient({ - baseURL: 'http://localhost:3000/api', +const orpcLink = new ORPCLink({ + url: 'http://localhost:3000/api', headers: () => ({ Authorization: 'Bearer default-token', }), }) +export const orpcClient = createORPCClient(orpcLink) + export const { orpc, ORPCContext } = createORPCReact() diff --git a/playgrounds/nuxt/lib/orpc.ts b/playgrounds/nuxt/lib/orpc.ts index 797baac4..94a28eb0 100644 --- a/playgrounds/nuxt/lib/orpc.ts +++ b/playgrounds/nuxt/lib/orpc.ts @@ -1,12 +1,15 @@ import type { router } from '~/server/router' -import { createORPCFetchClient } from '@orpc/client' +import { createORPCClient } from '@orpc/client' +import { ORPCLink } from '@orpc/client/fetch' import { createORPCVueQueryUtils } from '@orpc/vue-query' -export const client = createORPCFetchClient({ - baseURL: 'http://localhost:3000/api', +const orpcLink = new ORPCLink({ + url: 'http://localhost:3000/api', headers: () => ({ Authorization: 'Bearer default-token', }), }) +export const client = createORPCClient(orpcLink) + export const orpc = createORPCVueQueryUtils(client) diff --git a/playgrounds/openapi/src/playground-client.ts b/playgrounds/openapi/src/playground-client.ts index b53f16ce..e2351549 100644 --- a/playgrounds/openapi/src/playground-client.ts +++ b/playgrounds/openapi/src/playground-client.ts @@ -3,12 +3,15 @@ */ import type { router } from './router' -import { createORPCFetchClient } from '@orpc/client' +import { createORPCClient } from '@orpc/client' +import { ORPCLink } from '@orpc/client/fetch' -const orpc = createORPCFetchClient({ - baseURL: 'http://localhost:3000/api', +const orpcLink = new ORPCLink({ + url: 'http://localhost:3000/api', }) +const orpc = createORPCClient(orpcLink) + const planets = await orpc.planet.list({}) const planet = await orpc.planet.find({ id: 1 }) diff --git a/playgrounds/openapi/src/playground-react.ts b/playgrounds/openapi/src/playground-react.ts index 708f65b8..a9d55fa0 100644 --- a/playgrounds/openapi/src/playground-react.ts +++ b/playgrounds/openapi/src/playground-react.ts @@ -6,7 +6,7 @@ import type { RouterClient } from '@orpc/server' import type { router } from './router' import { createORPCReact } from '@orpc/react' -const { orpc } = createORPCReact >() +const { orpc } = createORPCReact >() const listQuery = orpc.planet.list.useInfiniteQuery({ input: {},