Skip to content

Commit e12f7c3

Browse files
dinwwwhclaude
andauthored
feat(pinia-colada): Pinia Colada integration (#1686)
## Summary Adds `@orpc/pinia-colada`, the [Pinia Colada](https://pinia-colada.esm.dev/) integration for oRPC v2 (successor of v1's `@orpc/vue-colada`), plus a [documentation page](https://orpc.dev/docs/integrations/pinia-colada). ```ts import { createPiniaColadaUtils } from '@orpc/pinia-colada' const orpc = createPiniaColadaUtils(client) // reactive inputs via the useQuery callback form, like defineQueryOptions const query = useQuery(() => orpc.planet.find.queryOptions({ input: { id: id.value } })) const pages = useInfiniteQuery(() => orpc.planet.list.infiniteOptions({ input: (offset: number) => ({ limit: 10, offset }), initialPageParam: 0, getNextPageParam: lastPage => lastPage.nextOffset, })) const mutation = useMutation(orpc.planet.create.mutationOptions()) queryCache.invalidateQueries({ key: orpc.planet.key() }) ``` ## What you get - `.queryOptions`, `.streamedOptions`, `.liveOptions`, `.infiniteOptions`, and `.mutationOptions` for `useQuery`, `useInfiniteQuery`, and `useMutation`, working with both the direct and callback (`() => options`) forms and composing with `defineQueryOptions` / `defineInfiniteQueryOptions` - Options take plain values only, following Pinia Colada's `defineQueryOptions` pattern — reactivity happens at the `useQuery` call site - Streamed and live queries for [AsyncIteratorObject](https://orpc.dev/docs/async-iterator-object) procedures: streamed queries accumulate chunks as they arrive (with `refetchMode` and `maxChunks`), live queries always show the latest chunk - `.key` for partial matching (invalidation), plus `.queryKey` / `.streamedKey` / `.liveKey` / `.infiniteKey` / `.mutationKey` returning tagged keys so cache reads like `queryCache.getQueryData` infer data types - Inputs are serialized into entry keys, so native types (`Date`, `URL`, `BigInt`, ...) work in keys out of the box - `prefix` option to mount multiple utils over the same client without key collisions - Feature parity with `@orpc/tanstack-query`: interceptors, per-procedure `scoped` defaults, plugins, and an operation context for configuring links per operation type - Targets `@pinia/colada` >= 1.0 ## Tests Unit, type, and e2e tests (real Vue components against an `RPCHandler`-backed client) with 100% coverage on all source files. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 62a40c4 commit e12f7c3

28 files changed

Lines changed: 4651 additions & 0 deletions

apps/content/.vitepress/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ export default withMermaid(defineConfig({
191191
{ text: 'NestJS', link: '/docs/integrations/nest' },
192192
{ text: 'Next.js', link: '/docs/integrations/next' },
193193
{ text: 'OpenTelemetry', link: '/docs/integrations/opentelemetry' },
194+
{ text: 'Pinia Colada', link: '/docs/integrations/pinia-colada' },
194195
{ text: 'Pino', link: '/docs/integrations/pino' },
195196
{ text: 'Tanstack Query', link: '/docs/integrations/tanstack-query' },
196197
{ text: 'tRPC', link: '/docs/integrations/trpc' },
Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
# Pinia Colada Integration
2+
3+
[Pinia Colada](https://pinia-colada.esm.dev/) integration provides utilities for using oRPC clients with Pinia Colada. It includes helper methods for building query and mutation options, as well as query and mutation keys.
4+
5+
::: warning
6+
This guide assumes you are already familiar with [Pinia Colada](https://pinia-colada.esm.dev/). If you need a refresher, review the official Pinia Colada documentation before continuing.
7+
:::
8+
9+
## Installation
10+
11+
::: code-group
12+
13+
```sh [npm]
14+
npm install @orpc/pinia-colada@beta
15+
```
16+
17+
```sh [yarn]
18+
yarn add @orpc/pinia-colada@beta
19+
```
20+
21+
```sh [pnpm]
22+
pnpm add @orpc/pinia-colada@beta
23+
```
24+
25+
```sh [bun]
26+
bun add @orpc/pinia-colada@beta
27+
```
28+
29+
```sh [deno]
30+
deno add npm:@orpc/pinia-colada@beta
31+
```
32+
33+
:::
34+
35+
## Setup
36+
37+
Before you begin, set up either a [server-side client](/docs/client/server-side) or a [client-side client](/docs/client/client-side).
38+
39+
```ts
40+
import { createPiniaColadaUtils } from '@orpc/pinia-colada'
41+
42+
const orpc = createPiniaColadaUtils(client)
43+
```
44+
45+
::: details Avoiding Query and Mutation Key Conflicts?
46+
47+
To avoid key conflicts when creating multiple sets of utils, pass a unique `prefix`. It becomes the first element of every entry key, so entries from different utils never overlap.
48+
49+
```ts
50+
const userORPC = createPiniaColadaUtils(userClient, {
51+
prefix: 'user'
52+
})
53+
54+
const postORPC = createPiniaColadaUtils(postClient, {
55+
prefix: 'post'
56+
})
57+
```
58+
59+
:::
60+
61+
## Query Options Utility
62+
63+
Use `.queryOptions` to build query options. It works with `useQuery` and any other API that accepts query options.
64+
65+
```ts
66+
const query = useQuery(orpc.planet.find.queryOptions({
67+
input: { id: 123 }, // Specify input if needed
68+
context: { cache: true }, // Provide client context if needed
69+
// additional options...
70+
}))
71+
```
72+
73+
::: info
74+
Options accept plain values only. For reactive inputs, pass a callback to `useQuery` as described in [Reactive Options](#reactive-options).
75+
:::
76+
77+
## Streamed Query Options Utility
78+
79+
Use `.streamedOptions` to build streamed query options for an [AsyncIteratorObject](/docs/async-iterator-object). The resulting data is an array of chunks, and each new chunk is appended as it arrives. It works with `useQuery` and any other API that accepts query options.
80+
81+
```ts
82+
const query = useQuery(orpc.streamed.streamedOptions({
83+
input: { id: 123 }, // Specify input if needed
84+
context: { cache: true }, // Provide client context if needed
85+
fnOptions: { // Configure streamed query behavior
86+
refetchMode: 'reset',
87+
maxChunks: 3,
88+
},
89+
// additional options...
90+
}))
91+
```
92+
93+
::: info
94+
`refetchMode` determines how data is handled when the query is fetched again:
95+
96+
- `'reset'` _(default)_: Clears existing data and returns the query to a pending state.
97+
- `'append'`: Adds new streamed chunks to the existing data.
98+
- `'replace'`: Buffers streamed data and replaces the cache after the stream completes.
99+
100+
:::
101+
102+
## Live Query Options Utility
103+
104+
Use `.liveOptions` to build live query options for an [AsyncIteratorObject](/docs/async-iterator-object). The data always reflects the latest chunk, replacing the previous value whenever a new one arrives. It works with `useQuery` and any other API that accepts query options.
105+
106+
```ts
107+
const query = useQuery(orpc.live.liveOptions({
108+
input: { id: 123 }, // Specify input if needed
109+
context: { cache: true }, // Provide client context if needed
110+
// additional options...
111+
}))
112+
```
113+
114+
## Infinite Query Options Utility
115+
116+
Use `.infiniteOptions` to build infinite query options. It works with `useInfiniteQuery` and any other API that accepts infinite query options.
117+
118+
::: info
119+
The `input` option must be a function that receives the page parameter and returns the query input. Define the `pageParam` type explicitly if it can be `null` or `undefined`.
120+
:::
121+
122+
```ts
123+
const query = useInfiniteQuery(() => orpc.planet.list.infiniteOptions({
124+
input: (offset: number) => ({ limit: 10, offset }),
125+
context: { cache: true }, // Provide client context if needed
126+
initialPageParam: 0,
127+
getNextPageParam: lastPage => lastPage.nextOffset,
128+
// additional options...
129+
}))
130+
```
131+
132+
## Mutation Options
133+
134+
Use `.mutationOptions` to build mutation options. It works with `useMutation` and any other API that accepts mutation options.
135+
136+
```ts
137+
const mutation = useMutation(orpc.planet.create.mutationOptions({
138+
context: { cache: true }, // Provide client context if needed
139+
// additional options...
140+
}))
141+
142+
mutation.mutate({ name: 'Earth' })
143+
```
144+
145+
## Query/Mutation Key
146+
147+
oRPC provides helper methods for generating query and mutation keys:
148+
149+
- `.key`: Generates a **partial-match** key for actions such as invalidating queries or checking mutation status.
150+
- `.queryKey`: Generates a **full-match** key for [Query Options](#query-options-utility).
151+
- `.streamedKey`: Generates a **full-match** key for [Streamed Query Options](#streamed-query-options-utility).
152+
- `.liveKey`: Generates a **full-match** key for [Live Query Options](#live-query-options-utility).
153+
- `.infiniteKey`: Generates a **full-match** key for [Infinite Query Options](#infinite-query-options-utility).
154+
- `.mutationKey`: Generates a **full-match** key for [Mutation Options](#mutation-options).
155+
156+
```ts
157+
const queryCache = useQueryCache()
158+
159+
// Invalidate all planet queries
160+
queryCache.invalidateQueries({
161+
key: orpc.planet.key(),
162+
})
163+
164+
// Invalidate only regular (non-infinite) planet queries
165+
queryCache.invalidateQueries({
166+
key: orpc.planet.key({ type: 'query' })
167+
})
168+
169+
// Invalidate the planet find query with id 123
170+
queryCache.invalidateQueries({
171+
key: orpc.planet.find.key({ input: { id: 123 } })
172+
})
173+
174+
// Update the planet find query with id 123
175+
queryCache.setQueryData(orpc.planet.find.queryKey({ input: { id: 123 } }), (old) => {
176+
return { ...old, id: 123, name: 'Earth' }
177+
})
178+
```
179+
180+
::: info
181+
Because Pinia Colada requires entry keys to be serializable, oRPC serializes inputs into JSON-compatible values (including native types like `Date`, `URL`, `BigInt`, etc.) when building keys.
182+
:::
183+
184+
## Calling Procedure Clients
185+
186+
The `.call` method provides direct access to the underlying procedure client when needed.
187+
188+
```ts
189+
const planet = await orpc.planet.find.call({ id: 123 })
190+
```
191+
192+
## Reactive Options
193+
194+
Option utilities accept plain values only. For reactive inputs, pass a callback to `useQuery` instead — it re-evaluates whenever its dependencies change.
195+
196+
```ts
197+
const id = ref(123)
198+
199+
const query = useQuery(() => orpc.planet.find.queryOptions({
200+
input: { id: id.value },
201+
}))
202+
```
203+
204+
## Default Options
205+
206+
Use `scoped` to configure default options for scoped query and mutation utilities. Each value can be either a partial options object, which is spread-merged with lower priority than per-call options, or a function that receives the per-call options and returns the merged result.
207+
208+
```ts
209+
const orpc = createPiniaColadaUtils(client, {
210+
scoped: {
211+
planet: {
212+
find: {
213+
queryKey: options => ({
214+
// Override the auto-generated key for .queryKey and .queryOptions
215+
key: options.key ?? ['planet', 'find', options.input]
216+
}),
217+
queryOptions: {
218+
staleTime: 60 * 1000, // 1 minute
219+
},
220+
},
221+
create: {
222+
mutationOptions: {
223+
onSuccess: () => {
224+
// runs for every planet.create mutation
225+
},
226+
},
227+
},
228+
},
229+
},
230+
})
231+
232+
// These calls automatically use the default options
233+
const query = useQuery(orpc.planet.find.queryOptions({ input: { id: 123 } }))
234+
const mutation = useMutation(orpc.planet.create.mutationOptions())
235+
236+
// User-provided options take precedence
237+
const customQuery = useQuery(orpc.planet.find.queryOptions({
238+
input: { id: 123 },
239+
staleTime: 0, // overrides the default staleTime
240+
}))
241+
```
242+
243+
::: info
244+
When you configure `queryKey`, it also affects `.queryOptions` because it is used internally to generate keys. The same applies to infinite and mutation options when you configure their keys.
245+
:::
246+
247+
## Interceptors
248+
249+
Interceptors let you wrap `query` and `mutation` calls. Unlike [default options](#default-options), which can be overridden by per-call options, interceptors always run for every query and mutation.
250+
251+
```ts
252+
import { isInferableError, safe } from '@orpc/client'
253+
254+
const orpc = createPiniaColadaUtils(client, {
255+
queryInterceptors: [],
256+
streamedInterceptors: [],
257+
liveInterceptors: [],
258+
infiniteInterceptors: [],
259+
mutationInterceptors: [
260+
async ({ context, path, next }) => {
261+
const [error, data] = await safe(next())
262+
263+
if (error) {
264+
if (isInferableError(error)) {
265+
// handle typesafe errors
266+
}
267+
268+
throw error
269+
}
270+
271+
return data
272+
}
273+
],
274+
})
275+
```
276+
277+
::: info
278+
You can use [`safe` and `isInferableError`](/docs/client/error-handling#using-safe-and-isinferableerror) together for typesafe error handling in interceptors.
279+
:::
280+
281+
## Plugins
282+
283+
Plugins package reusable defaults and interceptors for queries and mutations.
284+
285+
```ts
286+
const orpc = createPiniaColadaUtils(client, {
287+
plugins: []
288+
})
289+
```
290+
291+
## Client Context
292+
293+
When a client is invoked through the Pinia Colada integration, an **operation context** is automatically added to the [client context](/docs/client/client-side#client-context). You can use this context to configure request behavior, such as selecting the HTTP method for [RPC Link](/docs/rpc/link#request-method).
294+
295+
```ts
296+
import {
297+
PINIA_COLADA_OPERATION_CONTEXT_SYMBOL,
298+
PiniaColadaOperationContext,
299+
} from '@orpc/pinia-colada'
300+
import { RPCLink } from '@orpc/client/fetch'
301+
302+
interface ClientContext extends PiniaColadaOperationContext {
303+
}
304+
305+
const GET_OPERATION_TYPE = new Set(['query', 'streamed', 'live', 'infinite'])
306+
307+
const link = new RPCLink<ClientContext>({
308+
method: ({ context }) => {
309+
const operationType = context[PINIA_COLADA_OPERATION_CONTEXT_SYMBOL]?.type
310+
311+
if (operationType && GET_OPERATION_TYPE.has(operationType)) {
312+
return 'GET'
313+
}
314+
315+
return 'POST'
316+
},
317+
})
318+
```
319+
320+
## Typesafe Error Handling
321+
322+
Use the built-in `isInferableError` helper to handle [typesafe errors](/docs/error-handling#typesafe-errors) in queries and mutations.
323+
324+
```ts
325+
import { isInferableError } from '@orpc/client'
326+
327+
const mutation = useMutation(orpc.planet.create.mutationOptions({
328+
onError: (error) => {
329+
if (isInferableError(error)) {
330+
// Handle typesafe errors here
331+
}
332+
}
333+
}))
334+
335+
mutation.mutate({ name: 'Earth' })
336+
337+
if (mutation.error.value && isInferableError(mutation.error.value)) {
338+
// Handle the typesafe errors here
339+
}
340+
```

packages/pinia-colada/.gitignore

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Hidden folders and files
2+
.*
3+
!.gitignore
4+
!.*.example
5+
6+
# Common generated folders
7+
logs/
8+
node_modules/
9+
out/
10+
dist/
11+
dist-ssr/
12+
build/
13+
coverage/
14+
temp/
15+
16+
# Common generated files
17+
*.log
18+
*.log.*
19+
*.tsbuildinfo
20+
*.vitest-temp.json
21+
vite.config.ts.timestamp-*
22+
vitest.config.ts.timestamp-*
23+
24+
# Common manual ignore files
25+
*.local
26+
*.pem

0 commit comments

Comments
 (0)