Skip to content

Commit b455652

Browse files
authored
feat(pinia-colada): contract utils factory (#1700)
Ports `createContractUtilsFactory` and `createContractJsonifiedUtilsFactory` from `@orpc/tanstack-query` to `@orpc/pinia-colada`, so the [Scaling Large Projects](https://orpc.dev/docs/advanced/scaling-large-projects) pattern works with Pinia Colada too. ```ts import { createContractUtilsFactory } from '@orpc/pinia-colada' export const createUtils = createContractUtilsFactory(createClient, { /** options */ }) const utils = createUtils(procedure) const query = useQuery(utils.queryOptions({ /** options */ })) ``` - `RouterUtilsOptions` now carries the internal `path` option (aligned with `@orpc/tanstack-query`) - Docs: added a Pinia Colada section to the Scaling Large Projects guide
1 parent c2beea1 commit b455652

10 files changed

Lines changed: 456 additions & 8 deletions

File tree

apps/content/docs/advanced/scaling-large-projects.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,37 @@ const utils = createUtils(router)
122122

123123
const query = useQuery(utils.path.to.procedure.queryOptions({/** options */}))
124124
```
125+
126+
## Pinia Colada Integration
127+
128+
[Pinia Colada Integration](/docs/integrations/pinia-colada) also supports this pattern. First, create a factory that accepts a [contract client factory](#contract-client-factory) and options similar to the [Pinia Colada interceptor options](/docs/integrations/pinia-colada#interceptors), but with less type safety because the full contract is not known up front:
129+
130+
```ts
131+
import { createContractUtilsFactory } from '@orpc/pinia-colada'
132+
133+
export const createUtils = createContractUtilsFactory(createClient, { /** options */})
134+
```
135+
136+
::: warning
137+
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+
:::
139+
140+
You can then create utilities for each procedure contract:
141+
142+
```ts
143+
import { procedure } from './path/to/procedure'
144+
145+
const utils = createUtils(procedure)
146+
147+
const query = useQuery(utils.queryOptions({/** options */}))
148+
```
149+
150+
Like the [contract client factory](#contract-client-factory), it also accepts a [router contract](/docs/contract/router), returning utilities that mirror its shape:
151+
152+
```ts
153+
import { router } from './path/to/router'
154+
155+
const utils = createUtils(router)
156+
157+
const query = useQuery(utils.path.to.procedure.queryOptions({/** options */}))
158+
```

packages/pinia-colada/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
},
4444
"dependencies": {
4545
"@orpc/client": "workspace:*",
46+
"@orpc/contract": "workspace:*",
47+
"@orpc/openapi": "workspace:*",
4648
"@orpc/shared": "workspace:*"
4749
},
4850
"devDependencies": {
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import type { ORPCError } from '@orpc/client'
2+
import type { ContractClientFactory } from '@orpc/contract'
3+
import type { PromiseWithError } from '@orpc/shared'
4+
import type { ProcedureUtils } from './procedure-utils'
5+
import type { SharedRouterUtils } from './router-utils'
6+
import type { OperationContext } from './types'
7+
import { meta, oc, type } from '@orpc/contract'
8+
import { createContractJsonifiedUtilsFactory, createContractUtilsFactory } from './contract-utils'
9+
10+
const contract = {
11+
ping: oc.meta(meta.path(['ping'])),
12+
nested: {
13+
pong: oc
14+
.meta(meta.path(['nested', 'pong']))
15+
.errors({ BAD_GATEWAY: { data: type<string, RegExp>(vi.fn()) } })
16+
.input(type<string, boolean>(vi.fn()))
17+
.output(type<number, Date>(vi.fn())),
18+
},
19+
}
20+
21+
describe('createContractUtilsFactory', () => {
22+
const clientFactory = {} as ContractClientFactory<{ cache?: boolean }>
23+
24+
it('infers interceptor input, output, errors types', () => {
25+
createContractUtilsFactory(clientFactory, {
26+
mutationInterceptors: [
27+
async ({ context, next, input }) => {
28+
expectTypeOf(input).toEqualTypeOf<unknown>()
29+
expectTypeOf(context).toEqualTypeOf<{ cache?: boolean } & OperationContext>()
30+
31+
const result = next()
32+
33+
expectTypeOf(result).toEqualTypeOf<
34+
PromiseWithError<unknown, unknown>
35+
>()
36+
37+
return result
38+
},
39+
],
40+
scoped: {
41+
ping: {
42+
queryInterceptors: [
43+
async ({ context, next, input }) => {
44+
expectTypeOf(input).toEqualTypeOf<unknown>()
45+
expectTypeOf(context).toEqualTypeOf<{ cache?: boolean } & OperationContext>()
46+
47+
const result = next()
48+
49+
expectTypeOf(result).toEqualTypeOf<
50+
PromiseWithError<unknown, Error | ORPCError<string, unknown>>
51+
>()
52+
53+
return result
54+
},
55+
],
56+
},
57+
nested: {
58+
pong: {
59+
mutationInterceptors: [
60+
async ({ context, next, input }) => {
61+
expectTypeOf(input).toEqualTypeOf<unknown>()
62+
expectTypeOf(context).toEqualTypeOf<{ cache?: boolean } & OperationContext>()
63+
64+
const result = next()
65+
66+
expectTypeOf(result).toEqualTypeOf<
67+
PromiseWithError<unknown, Error | ORPCError<string, unknown>>
68+
>()
69+
70+
return result
71+
},
72+
],
73+
},
74+
},
75+
},
76+
})
77+
})
78+
79+
it('return general and procedure utils', () => {
80+
const createUtils = createContractUtilsFactory(clientFactory, {})
81+
const utils = createUtils(contract.nested.pong)
82+
83+
expectTypeOf(utils).toEqualTypeOf<
84+
& Omit<ProcedureUtils<{ cache?: boolean }, string, Date, Error | ORPCError<'BAD_GATEWAY', RegExp>>, 'path' | 'options'>
85+
& Omit<SharedRouterUtils<string>, 'path'>
86+
>()
87+
})
88+
89+
it('returns router utils when a router contract is passed', () => {
90+
const createUtils = createContractUtilsFactory(clientFactory, {})
91+
const utils = createUtils(contract)
92+
93+
expectTypeOf(utils.nested.pong).toEqualTypeOf<
94+
& Omit<ProcedureUtils<{ cache?: boolean }, string, Date, Error | ORPCError<'BAD_GATEWAY', RegExp>>, 'path' | 'options'>
95+
& Omit<SharedRouterUtils<string>, 'path'>
96+
>()
97+
98+
expectTypeOf(utils.key).toEqualTypeOf<SharedRouterUtils<unknown>['key']>()
99+
})
100+
})
101+
102+
describe('createContractJsonifiedUtilsFactory', () => {
103+
const clientFactory = {} as ContractClientFactory<{ cache?: boolean }>
104+
105+
it('infers interceptor input, output, errors types', () => {
106+
createContractJsonifiedUtilsFactory(clientFactory, {
107+
mutationInterceptors: [
108+
async ({ context, next, input }) => {
109+
expectTypeOf(input).toEqualTypeOf<unknown>()
110+
expectTypeOf(context).toEqualTypeOf<{ cache?: boolean } & OperationContext>()
111+
112+
const result = next()
113+
114+
expectTypeOf(result).toEqualTypeOf<
115+
PromiseWithError<unknown, unknown>
116+
>()
117+
118+
return result
119+
},
120+
],
121+
scoped: {
122+
ping: {
123+
queryInterceptors: [
124+
async ({ context, next, input }) => {
125+
expectTypeOf(input).toEqualTypeOf<unknown>()
126+
expectTypeOf(context).toEqualTypeOf<{ cache?: boolean } & OperationContext>()
127+
128+
const result = next()
129+
130+
expectTypeOf(result).toEqualTypeOf<
131+
PromiseWithError<unknown, Error | ORPCError<string, unknown>>
132+
>()
133+
134+
return result
135+
},
136+
],
137+
},
138+
nested: {
139+
pong: {
140+
mutationInterceptors: [
141+
async ({ context, next, input }) => {
142+
expectTypeOf(input).toEqualTypeOf<unknown>()
143+
expectTypeOf(context).toEqualTypeOf<{ cache?: boolean } & OperationContext>()
144+
145+
const result = next()
146+
147+
expectTypeOf(result).toEqualTypeOf<
148+
PromiseWithError<unknown, Error | ORPCError<string, unknown>>
149+
>()
150+
151+
return result
152+
},
153+
],
154+
},
155+
},
156+
},
157+
})
158+
})
159+
160+
it('return jsonified general and procedure utils', () => {
161+
const createUtils = createContractJsonifiedUtilsFactory(clientFactory, {})
162+
const utils = createUtils(contract.nested.pong)
163+
164+
expectTypeOf(utils).toEqualTypeOf<
165+
& Omit<ProcedureUtils<{ cache?: boolean }, string, string, Error | ORPCError<'BAD_GATEWAY', string>>, 'path' | 'options'>
166+
& Omit<SharedRouterUtils<string>, 'path'>
167+
>()
168+
})
169+
170+
it('returns jsonified router utils when a router contract is passed', () => {
171+
const createUtils = createContractJsonifiedUtilsFactory(clientFactory, {})
172+
const utils = createUtils(contract)
173+
174+
expectTypeOf(utils.nested.pong).toEqualTypeOf<
175+
& Omit<ProcedureUtils<{ cache?: boolean }, string, string, Error | ORPCError<'BAD_GATEWAY', string>>, 'path' | 'options'>
176+
& Omit<SharedRouterUtils<string>, 'path'>
177+
>()
178+
})
179+
})
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { ProcedureContract } from '@orpc/contract'
2+
import { createContractJsonifiedUtilsFactory, createContractUtilsFactory } from './contract-utils'
3+
import * as routerUtilsModule from './router-utils'
4+
5+
const createRouterUtilsSpy = vi.spyOn(routerUtilsModule, 'createRouterUtils')
6+
7+
beforeEach(() => {
8+
vi.clearAllMocks()
9+
})
10+
11+
function createContract(path?: string[]) {
12+
return new ProcedureContract({
13+
errorMap: {},
14+
meta: path ? { '~path': path } : {},
15+
inputSchemas: [],
16+
outputSchemas: [],
17+
})
18+
}
19+
20+
describe('createContractUtilsFactory', () => {
21+
const clientFactory = vi.fn()
22+
23+
it('throws when no procedure contract defines meta.path', () => {
24+
const factory = createContractUtilsFactory(clientFactory as any, {})
25+
26+
expect(() => factory(createContract() as any)).toThrow(
27+
'ContractUtilsFactory: procedure contract must define `meta.path` that matches its path in the root router contract.',
28+
)
29+
30+
expect(() => factory({ users: { list: createContract() } } as any)).toThrow(
31+
'ContractUtilsFactory: procedure contract must define `meta.path` that matches its path in the root router contract.',
32+
)
33+
34+
expect(createRouterUtilsSpy).not.toHaveBeenCalled()
35+
})
36+
37+
it('passes the created client and resolved path to createRouterUtils', () => {
38+
const delegatedUtils = { queryOptions: vi.fn() }
39+
const queryInterceptor = vi.fn()
40+
const mutationInterceptor = vi.fn()
41+
const plugin = {
42+
name: 'test-plugin',
43+
init: vi.fn((options: unknown) => options),
44+
initProcedureOptions: vi.fn((_: string[], options: unknown) => options),
45+
}
46+
const scopedOptions = {
47+
queryOptions: {
48+
staleTime: 1000,
49+
},
50+
}
51+
const options = {
52+
prefix: '__prefix__',
53+
queryInterceptors: [queryInterceptor],
54+
mutationInterceptors: [mutationInterceptor],
55+
plugins: [plugin],
56+
scoped: {
57+
users: {
58+
list: scopedOptions,
59+
},
60+
},
61+
}
62+
const procedure = createContract(['users', 'list'])
63+
const delegatedClient = vi.fn()
64+
65+
clientFactory.mockReturnValueOnce(delegatedClient)
66+
createRouterUtilsSpy.mockReturnValueOnce(delegatedUtils as any)
67+
68+
const factory = createContractUtilsFactory(clientFactory as any, options as any)
69+
const result = factory(procedure as any)
70+
71+
expect(result).toBe(delegatedUtils)
72+
expect(clientFactory).toHaveBeenCalledTimes(1)
73+
expect(clientFactory).toHaveBeenCalledWith(procedure)
74+
75+
expect(createRouterUtilsSpy).toHaveBeenCalledTimes(1)
76+
expect(createRouterUtilsSpy).toHaveBeenCalledWith(delegatedClient, {
77+
...options,
78+
path: ['users', 'list'],
79+
scoped: scopedOptions,
80+
})
81+
})
82+
83+
it('resolves the base path when a router contract is passed', () => {
84+
const delegatedUtils = { queryOptions: vi.fn() }
85+
const delegatedClient = { list: vi.fn(), find: vi.fn() }
86+
const scopedListOptions = {
87+
queryOptions: {
88+
staleTime: 1000,
89+
},
90+
}
91+
const options = {
92+
prefix: '__prefix__',
93+
scoped: {
94+
users: {
95+
list: scopedListOptions,
96+
},
97+
},
98+
}
99+
const router = {
100+
list: createContract(['users', 'list']),
101+
find: createContract(['users', 'find']),
102+
}
103+
104+
clientFactory.mockReturnValueOnce(delegatedClient)
105+
createRouterUtilsSpy.mockReturnValueOnce(delegatedUtils as any)
106+
107+
const factory = createContractUtilsFactory(clientFactory as any, options as any)
108+
const result = factory(router as any)
109+
110+
expect(result).toBe(delegatedUtils)
111+
expect(clientFactory).toHaveBeenCalledTimes(1)
112+
expect(clientFactory).toHaveBeenCalledWith(router)
113+
114+
expect(createRouterUtilsSpy).toHaveBeenCalledTimes(1)
115+
expect(createRouterUtilsSpy).toHaveBeenCalledWith(delegatedClient, {
116+
...options,
117+
path: ['users'],
118+
scoped: {
119+
list: scopedListOptions,
120+
},
121+
})
122+
})
123+
})
124+
125+
describe('createContractJsonifiedUtilsFactory', () => {
126+
const clientFactory = vi.fn()
127+
128+
it('delegates to createRouterUtils with the resolved path', () => {
129+
const delegatedUtils = { mutationOptions: vi.fn() }
130+
const scopedOptions = {
131+
mutationOptions: {
132+
meta: {
133+
feature: 'jsonified',
134+
},
135+
},
136+
}
137+
const options = {
138+
prefix: '__json__',
139+
scoped: {
140+
nested: {
141+
pong: scopedOptions,
142+
},
143+
},
144+
}
145+
const procedure = createContract(['nested', 'pong'])
146+
const delegatedClient = vi.fn()
147+
148+
clientFactory.mockReturnValueOnce(delegatedClient)
149+
createRouterUtilsSpy.mockReturnValueOnce(delegatedUtils as any)
150+
151+
const factory = createContractJsonifiedUtilsFactory(clientFactory as any, options as any)
152+
const result = factory(procedure as any)
153+
154+
expect(result).toBe(delegatedUtils)
155+
expect(clientFactory).toHaveBeenCalledTimes(1)
156+
expect(clientFactory).toHaveBeenCalledWith(procedure)
157+
158+
expect(createRouterUtilsSpy).toHaveBeenCalledTimes(1)
159+
expect(createRouterUtilsSpy).toHaveBeenCalledWith(delegatedClient, {
160+
...options,
161+
path: ['nested', 'pong'],
162+
scoped: scopedOptions,
163+
})
164+
})
165+
})

0 commit comments

Comments
 (0)