Skip to content

Commit ac20120

Browse files
committed
feat: useInfiniteQuery()
1 parent b3b8ce1 commit ac20120

File tree

9 files changed

+687
-101
lines changed

9 files changed

+687
-101
lines changed

eslint.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ export default eslintConfig({
55
nuxt: false,
66
tsconfigPath: './tsconfig.json',
77
}).append({
8+
rules: {
9+
'no-console': 'off',
10+
},
811
ignores: [
912
'.prettierrc.cjs',
1013
'.lintstagedrc.mjs',

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,19 +53,21 @@
5353
"@trpc/server": "11.0.0-rc.490",
5454
"@types/eslint": "^9.6.1",
5555
"@types/eslint__eslintrc": "^2.1.2",
56+
"@types/ws": "^8.5.12",
5657
"@vitest/ui": "^2.1.1",
5758
"eslint": "^9.11.0",
58-
"happy-dom": "^15.7.4",
5959
"husky": "^9.1.6",
6060
"lint-staged": "^15.2.10",
6161
"prettier": "^3.3.3",
6262
"start-server-and-test": "^2.0.8",
6363
"tsup": "^8.3.0",
6464
"tsx": "^4.19.1",
65+
"type-fest": "^4.26.1",
6566
"typescript": "^5.6.2",
6667
"vitest": "^2.1.1",
6768
"vue": "^3.5.7",
68-
"vue-demi": "^0.14.10"
69+
"ws": "^8.18.0",
70+
"zod": "^3.23.8"
6971
},
7072
"changelogithub": {
7173
"extends": "gh:falcondev-it/configs/changelogithub"

pnpm-lock.yaml

Lines changed: 328 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.ts

Lines changed: 119 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,46 @@
1-
/* eslint-disable ts/no-unsafe-argument, ts/no-unsafe-assignment, ts/no-unsafe-return, ts/no-unsafe-member-access, ts/no-unsafe-call */
2-
import { type QueryClient, type QueryKey, useMutation, useQuery } from '@tanstack/vue-query'
1+
/* eslint-disable ts/no-unsafe-argument */
2+
/* eslint-disable ts/no-unsafe-assignment */
3+
4+
import {
5+
type InfiniteQueryPageParamsOptions,
6+
type QueryClient,
7+
skipToken,
8+
useInfiniteQuery,
9+
useMutation,
10+
useQuery,
11+
} from '@tanstack/vue-query'
312
import {
413
type CreateTRPCClientOptions,
5-
createTRPCClient,
6-
type inferRouterClient,
14+
type TRPCUntypedClient,
15+
createTRPCUntypedClient,
716
} from '@trpc/client'
817
import { createTRPCFlatProxy } from '@trpc/server'
918
import { createRecursiveProxy } from '@trpc/server/unstable-core-do-not-import'
1019
import { toRef, toRefs, toValue } from '@vueuse/core'
11-
import { computed, isReactive } from 'vue'
20+
import { computed, isReactive, onScopeDispose, shallowRef, watch } from 'vue'
1221

13-
import type { DecoratedProcedureRecord } from './types'
22+
import type { DecorateProcedure, DecoratedProcedureRecord } from './types'
1423
import type { AnyTRPCRouter } from '@trpc/server'
1524
import type { MaybeRefOrGetter } from '@vueuse/core'
25+
import type { UnionToIntersection } from 'type-fest'
26+
27+
type QueryType = 'query' | 'infinite'
28+
export type TRPCQueryKey = [readonly string[], { input?: unknown; type?: QueryType }?]
29+
30+
function getQueryKey(path: string[], input: unknown, type?: QueryType): TRPCQueryKey {
31+
const splitPath = path.flatMap((part) => part.split('.'))
1632

17-
function getQueryKey(path: string[], input: unknown): QueryKey {
18-
return input === undefined ? path : [...path, input]
33+
if (input === undefined && !type) {
34+
return splitPath.length > 0 ? [splitPath] : ([] as unknown as TRPCQueryKey)
35+
}
36+
37+
return [
38+
splitPath,
39+
{
40+
...(input !== undefined && input !== skipToken && { input }),
41+
...(type && { type }),
42+
},
43+
]
1944
}
2045

2146
function maybeToRefs(obj: MaybeRefOrGetter<Record<string, unknown>>) {
@@ -25,78 +50,135 @@ function maybeToRefs(obj: MaybeRefOrGetter<Record<string, unknown>>) {
2550

2651
function createVueQueryProxyDecoration<TRouter extends AnyTRPCRouter>(
2752
name: string,
28-
client: inferRouterClient<TRouter>,
53+
trpc: TRPCUntypedClient<TRouter>,
2954
queryClient: QueryClient,
3055
) {
31-
return createRecursiveProxy((opts) => {
32-
const args = opts.args
33-
34-
const path = [name, ...opts.path]
56+
return createRecursiveProxy(({ args, path: _path }) => {
57+
const path = [name, ..._path]
3558

3659
// The last arg is for instance `.useMutation` or `.useQuery`
37-
const lastProperty = path.pop()!
60+
const prop = path.pop()! as keyof UnionToIntersection<DecorateProcedure<any, TRouter>> | '_def'
3861

3962
const joinedPath = path.join('.')
40-
const [firstParam, secondParam] = args
63+
const [firstArg, ...rest] = args
64+
const opts = rest[0] || ({} as any)
4165

42-
if (lastProperty === '_def') {
66+
if (prop === '_def') {
4367
return { path }
4468
}
4569

46-
if (lastProperty === 'useQuery') {
47-
const { trpc, ...queryOptions } = secondParam || ({} as any)
70+
if (prop === 'query') {
71+
return trpc.query(joinedPath, firstArg, opts)
72+
}
73+
if (prop === 'useQuery') {
74+
const { trpc: trpcOptions, ...queryOptions } = opts
4875

4976
return useQuery({
50-
queryKey: computed(() => getQueryKey(path, toValue(firstParam))),
51-
queryFn: ({ queryKey, signal }) =>
52-
(client as any)[joinedPath].query(queryKey.at(-1), {
77+
queryKey: computed(() => getQueryKey(path, toValue(firstArg), 'query')),
78+
queryFn: async ({ queryKey, signal }) =>
79+
trpc.query(joinedPath, queryKey[1]?.input, {
5380
signal,
54-
...trpc,
81+
...trpcOptions,
5582
}),
5683
...maybeToRefs(queryOptions),
5784
})
5885
}
5986

60-
if (lastProperty === 'invalidate') {
87+
if (prop === 'invalidate') {
6188
return queryClient.invalidateQueries({
62-
queryKey: getQueryKey(path, toValue(firstParam)),
89+
queryKey: getQueryKey(path, toValue(firstArg), 'query'),
6390
})
6491
}
6592

66-
if (lastProperty === 'setQueryData') {
67-
return queryClient.setQueryData(getQueryKey(path, toValue(secondParam)), firstParam)
93+
if (prop === 'setQueryData') {
94+
return queryClient.setQueryData(getQueryKey(path, toValue(opts), 'query'), firstArg)
6895
}
6996

70-
if (lastProperty === 'key') {
71-
return getQueryKey(path, toValue(firstParam))
97+
if (prop === 'key') {
98+
return getQueryKey(path, toValue(firstArg), 'query')
7299
}
73100

74-
if (lastProperty === 'useMutation') {
75-
const { trpc, ...mutationOptions } = firstParam || ({} as any)
101+
if (prop === 'mutate') {
102+
return trpc.mutation(joinedPath, firstArg, opts)
103+
}
104+
if (prop === 'useMutation') {
105+
const { trpc: trpcOptions, ...mutationOptions } = firstArg || ({} as any)
76106

77107
return useMutation({
78-
mutationFn: (payload) =>
79-
(client as any)[joinedPath].mutate(payload, {
80-
...trpc,
108+
mutationKey: computed(() => getQueryKey(path, undefined)),
109+
mutationFn: async (payload) =>
110+
trpc.mutation(joinedPath, payload, {
111+
...trpcOptions,
81112
}),
82113
...maybeToRefs(mutationOptions),
83114
})
84115
}
85116

86-
return (client as any)[joinedPath][lastProperty](...args)
117+
if (prop === 'subscribe') {
118+
return trpc.subscription(joinedPath, firstArg, opts)
119+
}
120+
if (prop === 'useSubscription') {
121+
const inputData = toRef(firstArg)
122+
123+
const subscription = shallowRef<ReturnType<(typeof trpc)['subscription']>>()
124+
watch(
125+
inputData,
126+
() => {
127+
subscription.value?.unsubscribe()
128+
129+
subscription.value = trpc.subscription(joinedPath, inputData.value, {
130+
...opts,
131+
})
132+
},
133+
{ immediate: true },
134+
)
135+
136+
onScopeDispose(() => {
137+
subscription.value?.unsubscribe()
138+
}, true)
139+
140+
return subscription.value!
141+
}
142+
143+
if (prop === 'useInfiniteQuery') {
144+
const { trpc: trpcOptions, ...queryOptions } = opts
145+
146+
return useInfiniteQuery({
147+
queryKey: computed(() => getQueryKey(path, toValue(firstArg), 'infinite')),
148+
queryFn: async ({ queryKey, pageParam, signal }) =>
149+
trpc.query(
150+
joinedPath,
151+
{
152+
...(queryKey[1]?.input as object),
153+
cursor: pageParam,
154+
},
155+
{
156+
signal,
157+
...trpcOptions,
158+
},
159+
),
160+
...(maybeToRefs(queryOptions) as InfiniteQueryPageParamsOptions),
161+
})
162+
}
163+
164+
// return (trpc as any)[joinedPath][prop](...args)
165+
throw new Error(`Method '.${prop as string}()' not supported`)
87166
})
88167
}
89168

90-
export function createTRPCVueQueryClient<TRouter extends AnyTRPCRouter>(opts: {
169+
export function createTRPCVueQueryClient<TRouter extends AnyTRPCRouter>({
170+
trpc,
171+
queryClient,
172+
}: {
91173
queryClient: QueryClient
92174
trpc: CreateTRPCClientOptions<TRouter>
93175
}) {
94-
const client = createTRPCClient<TRouter>(opts.trpc)
176+
const client = createTRPCUntypedClient<TRouter>(trpc)
95177

96178
const decoratedClient = createTRPCFlatProxy<
97179
DecoratedProcedureRecord<TRouter['_def']['record'], TRouter>
98180
>((key) => {
99-
return createVueQueryProxyDecoration(key, client as any, opts.queryClient)
181+
return createVueQueryProxyDecoration(key, client, queryClient)
100182
})
101183

102184
return decoratedClient

src/types.ts

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import type {
2+
InfiniteData,
3+
InitialPageParam,
24
QueryClient,
35
QueryKey,
6+
UseInfiniteQueryOptions,
7+
UseInfiniteQueryReturnType,
48
UseMutationOptions,
59
UseMutationReturnType,
610
UseQueryOptions,
711
UseQueryReturnType,
812
} from '@tanstack/vue-query'
9-
import type { TRPCClientErrorLike, TRPCRequestOptions } from '@trpc/client'
13+
import type { OperationContext, TRPCClientErrorLike, TRPCRequestOptions } from '@trpc/client'
1014
import type {
1115
AnyTRPCMutationProcedure,
1216
AnyTRPCProcedure,
@@ -17,12 +21,12 @@ import type {
1721
inferProcedureOutput,
1822
inferTransformedProcedureOutput,
1923
} from '@trpc/server'
20-
import type { Unsubscribable, inferObservableValue } from '@trpc/server/observable'
24+
import type { Unsubscribable } from '@trpc/server/observable'
2125
import type { ProcedureOptions } from '@trpc/server/unstable-core-do-not-import'
2226
import type { MaybeRefOrGetter, UnwrapRef } from 'vue'
2327

2428
type TRPCSubscriptionObserver<TValue, TError> = {
25-
onStarted: () => void
29+
onStarted: (opts: { context: OperationContext | undefined }) => void
2630
onData: (value: TValue) => void
2731
onError: (err: TError) => void
2832
onStopped: () => void
@@ -38,10 +42,7 @@ type SubscriptionResolver<TProcedure extends AnyTRPCProcedure, TRouter extends A
3842
input: inferProcedureInput<TProcedure>,
3943
opts: ProcedureOptions &
4044
Partial<
41-
TRPCSubscriptionObserver<
42-
inferObservableValue<inferProcedureOutput<TProcedure>>,
43-
TRPCClientErrorLike<TRouter>
44-
>
45+
TRPCSubscriptionObserver<inferProcedureOutput<TProcedure>, TRPCClientErrorLike<TRouter>>
4546
>,
4647
) => Unsubscribable
4748

@@ -75,7 +76,32 @@ export type DecorateProcedure<
7576
input?: MaybeRefOrGetter<inferProcedureInput<TProcedure>>,
7677
) => ReturnType<QueryClient['setQueryData']>
7778
key: (input?: MaybeRefOrGetter<inferProcedureInput<TProcedure>>) => QueryKey
78-
}
79+
} & (TProcedure['_def']['$types']['input'] extends { cursor?: infer CursorType }
80+
? {
81+
useInfiniteQuery: <
82+
TQueryFnData = inferTransformedProcedureOutput<TRouter, TProcedure>,
83+
TError = TRPCClientErrorLike<TRouter>,
84+
TData = InfiniteData<TQueryFnData>,
85+
TQueryData = TQueryFnData,
86+
TQueryKey extends QueryKey = QueryKey,
87+
>(
88+
input: MaybeRefOrGetter<Omit<inferProcedureInput<TProcedure>, 'cursor'>>,
89+
opts?: MaybeRefOrGetter<
90+
Omit<
91+
UnwrapRef<
92+
UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
93+
>,
94+
'queryKey' | keyof InitialPageParam
95+
> & {
96+
trpc?: TRPCRequestOptions
97+
queryKey?: TQueryKey
98+
} & (undefined extends TProcedure['_def']['$types']['input']['cursor']
99+
? Partial<InitialPageParam<CursorType>>
100+
: InitialPageParam<CursorType>)
101+
>,
102+
) => UseInfiniteQueryReturnType<TData, TError>
103+
}
104+
: object)
79105
: TProcedure extends AnyTRPCMutationProcedure
80106
? {
81107
mutate: Resolver<TRouter, TProcedure>
@@ -93,8 +119,20 @@ export type DecorateProcedure<
93119
) => UseMutationReturnType<TData, TError, TVariables, TContext>
94120
}
95121
: TProcedure extends AnyTRPCSubscriptionProcedure
96-
? { subscribe: SubscriptionResolver<TProcedure, TRouter> }
97-
: 'test'
122+
? {
123+
subscribe: SubscriptionResolver<TProcedure, TRouter>
124+
useSubscription: (
125+
input: MaybeRefOrGetter<inferProcedureInput<TProcedure>>,
126+
opts: ProcedureOptions &
127+
Partial<
128+
TRPCSubscriptionObserver<
129+
inferProcedureOutput<TProcedure>,
130+
TRPCClientErrorLike<TRouter>
131+
>
132+
>,
133+
) => Unsubscribable
134+
}
135+
: never
98136

99137
/**
100138
* @internal

0 commit comments

Comments
 (0)