Skip to content

Commit b3945c9

Browse files
dinwwwhclaude
andauthored
fix!: only accept jsonified client factories in createContractJsonifiedUtilsFactory (#2328)
## Problem [Contract Client Factory](https://orpc.dev/docs/contract/client-factory) tells OpenAPI Link users two things: - create the client factory with `createContractJsonifiedClientFactory` from `@orpc/openapi`; - create the utils with `createContractJsonifiedUtilsFactory` from `@orpc/tanstack-query` / `@orpc/pinia-colada`. Following both fails, because the utils factory only accepted a plain `ContractClientFactory`: ```ts const createClient = createContractJsonifiedClientFactory(link, { contractRef }) const createUtils = createContractJsonifiedUtilsFactory(createClient, {}) // TS2345: Argument of type 'ContractJsonifiedClientFactory<ClientContext>' is not assignable to parameter of type // 'ContractClientFactory<ClientContext>'. Type 'JsonifiedClient<RouterContractClient<T, ClientContext>>' is not // assignable to type 'RouterContractClient<T, ClientContext>'. ``` ## Fix In both `@orpc/tanstack-query` and `@orpc/pinia-colada`, `createContractJsonifiedUtilsFactory` now takes the jsonified client factory: ```diff - clientFactory: ContractClientFactory<TClientContext>, + clientFactory: ContractJsonifiedClientFactory<TClientContext>, ``` The jsonified utils factory always produces utils for `JsonifiedClient`, so its input is now the client factory that declares jsonified clients. This mirrors `createContractUtilsFactory`, which takes only a `ContractClientFactory`: | Client factory | Utils factory | | --- | --- | | `createContractClientFactory` (`@orpc/contract`) | `createContractUtilsFactory` | | `createContractJsonifiedClientFactory` (`@orpc/openapi`) | `createContractJsonifiedUtilsFactory` | Passing the other kind to either utils factory is now a type error. Previously a plain client factory passed to the jsonified utils factory was silently re-typed as jsonified. `createContractJsonifiedClientFactory` is `createContractClientFactory(...)` cast to `ContractJsonifiedClientFactory`, so there is no runtime change. Neither factory type is assignable to the other, so the restriction is enforced in both directions. Docs: - **`createContractJsonifiedUtilsFactory`** gets a JSDoc block with a `@see` link and the same `meta.path` note as `createContractUtilsFactory`. - **`createContractJsonifiedClientFactory`** (`@orpc/openapi`) gets a JSDoc mirroring `createContractClientFactory`'s. It had none, and this PR makes it the required input. - **Docs page:** the two warnings (TanStack Query and Pinia Colada sections) now say to pass a client factory created by `createContractJsonifiedClientFactory`. ## Breaking change `createContractJsonifiedUtilsFactory` no longer accepts a plain `ContractClientFactory` (types only). Create the client factory with `createContractJsonifiedClientFactory` from `@orpc/openapi` instead. ## Tests In both packages, the `createContractJsonifiedUtilsFactory` cases in `contract-utils.test-d.ts` now use a `ContractJsonifiedClientFactory`. They cover interceptor input/output/error types and the procedure and router utils types. Before this PR they failed with the TS2345 above. ## Verification - `pnpm lint`, `pnpm vitest run packages/tanstack-query packages/pinia-colada`, `pnpm type:check` (which includes the playgrounds) and `pnpm docs:validate` pass. - **Consumer projects**, against packed tarballs of every package from `main` and from this branch: - Setup: one TanStack Query consumer and one Pinia Colada consumer, each installing only `@orpc/contract`, `@orpc/client`, `@orpc/openapi` and the integration. They follow the docs: OpenAPI Link with `contractRef`, a required `{ token: string }` context, and scoped options. - Builds: `composite: true` with TS 6 (bundler and NodeNext) and TS 5.9. - On `main`: the docs flow fails with the TS2345 above, and a plain client factory passed to the jsonified utils factory is still accepted. - On this branch: zero diagnostics, and passing the wrong kind of client factory to either utils factory is a type error. The required context reaches every interceptor and call site, jsonified output types reach `queryOptions`/`fetchQuery`/`mutationOptions`, and the emitted JS is byte-identical to `main`. - **Not caused by this PR:** re-checking the emitted `.d.ts` with `skipLibCheck: false` reports TS2304 for `dataTagSymbol` when a consumer exports an inferred `queryKey`. This happens identically on `main` and with `@tanstack/query-core` alone. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01U63hBfpeESmJiHXznRFzjN --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0e59dd2 commit b3945c9

6 files changed

Lines changed: 53 additions & 22 deletions

File tree

‎apps/content/docs/contract/client-factory.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export const createUtils = createContractUtilsFactory(createClient, { /** option
101101
```
102102

103103
:::warning
104-
If you are using [OpenAPI Link](/docs/openapi/link), or any link that requires the client to be wrapped in `JsonifiedClient`, use `createContractJsonifiedUtilsFactory` from `@orpc/tanstack-query` instead of `createContractUtilsFactory`.
104+
If you are using [OpenAPI Link](/docs/openapi/link), or any link that requires the client to be wrapped in `JsonifiedClient`, use `createContractJsonifiedUtilsFactory` from `@orpc/tanstack-query` instead of `createContractUtilsFactory`, and pass it a client factory created by [`createContractJsonifiedClientFactory`](#contract-client-factory).
105105
:::
106106

107107
You can then create utilities for each procedure contract:
@@ -135,7 +135,7 @@ export const createUtils = createContractUtilsFactory(createClient, { /** option
135135
```
136136

137137
:::warning
138-
If you are using [OpenAPI Link](/docs/openapi/link), or any link that requires the client to be wrapped in `JsonifiedClient`, use `createContractJsonifiedUtilsFactory` from `@orpc/pinia-colada` instead of `createContractUtilsFactory`.
138+
If you are using [OpenAPI Link](/docs/openapi/link), or any link that requires the client to be wrapped in `JsonifiedClient`, use `createContractJsonifiedUtilsFactory` from `@orpc/pinia-colada` instead of `createContractUtilsFactory`, and pass it a client factory created by [`createContractJsonifiedClientFactory`](#contract-client-factory).
139139
:::
140140

141141
You can then create utilities for each procedure contract:

‎packages/openapi/src/client-factory.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ export interface ContractJsonifiedClientFactoryOptions<
2020
contractRef?: RouterContract | undefined
2121
}
2222

23+
/**
24+
* Like `createContractClientFactory`, but the clients it builds are typed as `JsonifiedClient`,
25+
* for links that require it, such as OpenAPI Link.
26+
*
27+
* @remarks
28+
* **Warning**: Every procedure contract passed to the factory must define `meta.path` matching its location in the root contract.
29+
*
30+
* @see {@link https://orpc.dev/docs/contract/client-factory | Contract Client Factory}
31+
*/
2332
export function createContractJsonifiedClientFactory<
2433
TClientContext extends ClientContext,
2534
>(

‎packages/pinia-colada/src/contract-utils.test-d.ts‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ORPCError } from '@orpc/client'
22
import type { ContractClientFactory } from '@orpc/contract'
3+
import type { ContractJsonifiedClientFactory } from '@orpc/openapi'
34
import type { PromiseWithError } from '@orpc/shared'
45
import type { ProcedureUtils } from './procedure-utils'
56
import type { SharedUtils } from './shared-utils'
@@ -18,9 +19,10 @@ const contract = {
1819
},
1920
}
2021

21-
describe('createContractUtilsFactory', () => {
22-
const clientFactory = {} as ContractClientFactory<{ cache?: boolean }>
22+
const clientFactory = {} as ContractClientFactory<{ cache?: boolean }>
23+
const jsonifiedClientFactory = {} as ContractJsonifiedClientFactory<{ cache?: boolean }>
2324

25+
describe('createContractUtilsFactory', () => {
2426
it('infers interceptor input, output, errors types', () => {
2527
createContractUtilsFactory(clientFactory, {
2628
mutationInterceptors: [
@@ -98,10 +100,8 @@ describe('createContractUtilsFactory', () => {
98100
})
99101

100102
describe('createContractJsonifiedUtilsFactory', () => {
101-
const clientFactory = {} as ContractClientFactory<{ cache?: boolean }>
102-
103103
it('infers interceptor input, output, errors types', () => {
104-
createContractJsonifiedUtilsFactory(clientFactory, {
104+
createContractJsonifiedUtilsFactory(jsonifiedClientFactory, {
105105
mutationInterceptors: [
106106
async ({ context, next, input }) => {
107107
expectTypeOf(input).toEqualTypeOf<unknown>()
@@ -156,7 +156,7 @@ describe('createContractJsonifiedUtilsFactory', () => {
156156
})
157157

158158
it('return jsonified general and procedure utils', () => {
159-
const createUtils = createContractJsonifiedUtilsFactory(clientFactory, {})
159+
const createUtils = createContractJsonifiedUtilsFactory(jsonifiedClientFactory, {})
160160
const utils = createUtils(contract.nested.pong)
161161

162162
expectTypeOf(utils).toEqualTypeOf<
@@ -165,7 +165,7 @@ describe('createContractJsonifiedUtilsFactory', () => {
165165
})
166166

167167
it('returns jsonified router utils when a router contract is passed', () => {
168-
const createUtils = createContractJsonifiedUtilsFactory(clientFactory, {})
168+
const createUtils = createContractJsonifiedUtilsFactory(jsonifiedClientFactory, {})
169169
const utils = createUtils(contract)
170170

171171
expectTypeOf(utils.nested.pong).toEqualTypeOf<

‎packages/pinia-colada/src/contract-utils.ts‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ClientContext } from '@orpc/client'
22
import type { ContractClientFactory, RouterContract, RouterContractClient } from '@orpc/contract'
3-
import type { JsonifiedClient } from '@orpc/openapi'
3+
import type { ContractJsonifiedClientFactory, JsonifiedClient } from '@orpc/openapi'
44
import type { RouterUtils, RouterUtilsOptions } from './router-utils'
55
import { resolveBasePathMeta } from '@orpc/contract'
66
import { get } from '@orpc/shared'
@@ -56,11 +56,22 @@ export interface ContractJsonifiedUtilsFactoryOptions<
5656
> extends Omit<RouterUtilsOptions<JsonifiedClient<RouterContractClient<RouterContract, TClientContext>>>, 'path'> {
5757
}
5858

59+
/**
60+
* Like `createContractUtilsFactory`, but for clients whose types are wrapped in `JsonifiedClient`,
61+
* such as clients using OpenAPI Link.
62+
*
63+
* @remarks
64+
* Takes a client factory created by `createContractJsonifiedClientFactory` from `@orpc/openapi`.
65+
*
66+
* **Note**: The contract must define a `path` meta matching its position in the root router contract.
67+
*
68+
* @see {@link https://orpc.dev/docs/contract/client-factory#pinia-colada-integration | Contract Client Factory - Pinia Colada Integration}
69+
*/
5970
export function createContractJsonifiedUtilsFactory<
6071
TClientContext extends ClientContext,
6172
>(
62-
clientFactory: ContractClientFactory<TClientContext>,
73+
clientFactory: ContractJsonifiedClientFactory<TClientContext>,
6374
options: ContractJsonifiedUtilsFactoryOptions<TClientContext>,
6475
): ContractJsonifiedUtilsFactory<TClientContext> {
65-
return createContractUtilsFactory(clientFactory, options) as ContractJsonifiedUtilsFactory<TClientContext>
76+
return createContractUtilsFactory(clientFactory as ContractClientFactory<TClientContext>, options) as ContractJsonifiedUtilsFactory<TClientContext>
6677
}

‎packages/tanstack-query/src/contract-utils.test-d.ts‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ORPCError } from '@orpc/client'
22
import type { ContractClientFactory } from '@orpc/contract'
3+
import type { ContractJsonifiedClientFactory } from '@orpc/openapi'
34
import type { PromiseWithError } from '@orpc/shared'
45
import type { ProcedureUtils } from './procedure-utils'
56
import type { SharedUtils } from './shared-utils'
@@ -18,9 +19,10 @@ const contract = {
1819
},
1920
}
2021

21-
describe('createContractUtilsFactory', () => {
22-
const clientFactory = {} as ContractClientFactory<{ cache?: boolean }>
22+
const clientFactory = {} as ContractClientFactory<{ cache?: boolean }>
23+
const jsonifiedClientFactory = {} as ContractJsonifiedClientFactory<{ cache?: boolean }>
2324

25+
describe('createContractUtilsFactory', () => {
2426
it('infers interceptor input, output, errors types', () => {
2527
createContractUtilsFactory(clientFactory, {
2628
mutationInterceptors: [
@@ -98,10 +100,8 @@ describe('createContractUtilsFactory', () => {
98100
})
99101

100102
describe('createContractJsonifiedUtilsFactory', () => {
101-
const clientFactory = {} as ContractClientFactory<{ cache?: boolean }>
102-
103103
it('infers interceptor input, output, errors types', () => {
104-
createContractJsonifiedUtilsFactory(clientFactory, {
104+
createContractJsonifiedUtilsFactory(jsonifiedClientFactory, {
105105
mutationInterceptors: [
106106
async ({ context, next, input }) => {
107107
expectTypeOf(input).toEqualTypeOf<unknown>()
@@ -156,7 +156,7 @@ describe('createContractJsonifiedUtilsFactory', () => {
156156
})
157157

158158
it('return jsonified general and procedure utils', () => {
159-
const createUtils = createContractJsonifiedUtilsFactory(clientFactory, {})
159+
const createUtils = createContractJsonifiedUtilsFactory(jsonifiedClientFactory, {})
160160
const utils = createUtils(contract.nested.pong)
161161

162162
expectTypeOf(utils).toEqualTypeOf<
@@ -165,7 +165,7 @@ describe('createContractJsonifiedUtilsFactory', () => {
165165
})
166166

167167
it('returns jsonified router utils when a router contract is passed', () => {
168-
const createUtils = createContractJsonifiedUtilsFactory(clientFactory, {})
168+
const createUtils = createContractJsonifiedUtilsFactory(jsonifiedClientFactory, {})
169169
const utils = createUtils(contract)
170170

171171
expectTypeOf(utils.nested.pong).toEqualTypeOf<

‎packages/tanstack-query/src/contract-utils.ts‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ClientContext } from '@orpc/client'
22
import type { ContractClientFactory, RouterContract, RouterContractClient } from '@orpc/contract'
3-
import type { JsonifiedClient } from '@orpc/openapi'
3+
import type { ContractJsonifiedClientFactory, JsonifiedClient } from '@orpc/openapi'
44
import type { RouterUtils, RouterUtilsOptions } from './router-utils'
55
import { resolveBasePathMeta } from '@orpc/contract'
66
import { get } from '@orpc/shared'
@@ -56,11 +56,22 @@ export interface ContractJsonifiedUtilsFactoryOptions<
5656
> extends Omit<RouterUtilsOptions<JsonifiedClient<RouterContractClient<RouterContract, TClientContext>>>, 'path'> {
5757
}
5858

59+
/**
60+
* Like `createContractUtilsFactory`, but for clients whose types are wrapped in `JsonifiedClient`,
61+
* such as clients using OpenAPI Link.
62+
*
63+
* @remarks
64+
* Takes a client factory created by `createContractJsonifiedClientFactory` from `@orpc/openapi`.
65+
*
66+
* **Note**: The contract must define a `path` meta matching its position in the root router contract.
67+
*
68+
* @see {@link https://orpc.dev/docs/contract/client-factory#tanstack-query-integration | Contract Client Factory - TanStack Query Integration}
69+
*/
5970
export function createContractJsonifiedUtilsFactory<
6071
TClientContext extends ClientContext,
6172
>(
62-
clientFactory: ContractClientFactory<TClientContext>,
73+
clientFactory: ContractJsonifiedClientFactory<TClientContext>,
6374
options: ContractJsonifiedUtilsFactoryOptions<TClientContext>,
6475
): ContractJsonifiedUtilsFactory<TClientContext> {
65-
return createContractUtilsFactory(clientFactory, options) as ContractJsonifiedUtilsFactory<TClientContext>
76+
return createContractUtilsFactory(clientFactory as ContractClientFactory<TClientContext>, options) as ContractJsonifiedUtilsFactory<TClientContext>
6677
}

0 commit comments

Comments
 (0)