-
- -
-
- Title: {{ t.title }}
-
-
-
+
+
+ Loading...
+
+
+ Error: {{ error.data.code }}
+
+
-
-
+
+
+
+ {{ JSON.stringify(todos) }}
+
diff --git a/playground/plugins/trpc-client.ts b/playground/plugins/trpc-client.ts
new file mode 100644
index 0000000..9f55432
--- /dev/null
+++ b/playground/plugins/trpc-client.ts
@@ -0,0 +1,110 @@
+import type { TRPCClientErrorLike, inferRouterProxyClient } from '@trpc/client'
+import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'
+import type { AnyMutationProcedure, AnyProcedure, AnyQueryProcedure, AnyRouter, ProcedureRecord, ProcedureRouterRecord, inferHandlerInput, inferProcedureInput, inferProcedureOutput, inferRouterInputs } from '@trpc/server'
+import { createFlatProxy, createRecursiveProxy } from '@trpc/server/shared'
+import type {
+ AsyncData, AsyncDataOptions, KeyOfRes, PickFrom, _Transform,
+} from 'nuxt/dist/app/composables/asyncData'
+import { hash } from 'ohash'
+import type { AppRouter } from '~~/server/trpc'
+
+/**
+ * Calculates the key used for `useAsyncData` call
+ */
+export function getQueryKey(
+ path: string,
+ input: unknown,
+): string {
+ return input === undefined ? path : `${path}-${hash(input || '')}`
+}
+
+function createNuxtProxyDecoration
(name: string, client: inferRouterProxyClient) {
+ return createRecursiveProxy((opts) => {
+ const args = opts.args
+
+ const pathCopy = [name, ...opts.path]
+
+ // The last arg is for instance `.mutate` or `.query()`
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const lastArg = pathCopy.pop()!
+
+ const path = pathCopy.join('.')
+
+ const [input, asyncDataOptions] = args
+
+ const queryKey = getQueryKey(path, input)
+
+ if (lastArg === 'mutate') {
+ return useAsyncData(queryKey, () => (client as any)[path][lastArg](input), {
+ ...asyncDataOptions as Record,
+ immediate: false,
+ })
+ }
+
+ return useAsyncData(queryKey, () => (client as any)[path][lastArg](input), asyncDataOptions as Record)
+ })
+}
+
+/**
+ * @internal
+ */
+export type DecorateProcedure<
+ TProcedure extends AnyProcedure,
+ TPath extends string,
+> = TProcedure extends AnyQueryProcedure
+ ? {
+ query: <
+ TData = inferProcedureOutput,
+ Transform extends _Transform = _Transform,
+ PickKeys extends KeyOfRes = KeyOfRes,
+ >(
+ input: inferProcedureInput,
+ opts?: AsyncDataOptions,
+ ) => AsyncData, PickKeys>, TRPCClientErrorLike>
+ } : TProcedure extends AnyMutationProcedure ? {
+ mutate: <
+ TData = inferProcedureOutput,
+ Transform extends _Transform = _Transform,
+ PickKeys extends KeyOfRes = KeyOfRes,
+ >(
+ input: inferProcedureInput,
+ opts?: AsyncDataOptions,
+ ) => AsyncData, PickKeys>, TRPCClientErrorLike>
+ } : never
+
+/**
+* @internal
+*/
+export type DecoratedProcedureRecord<
+ TProcedures extends ProcedureRouterRecord,
+ TPath extends string = '',
+> = {
+ [TKey in keyof TProcedures]: TProcedures[TKey] extends AnyRouter
+ ? DecoratedProcedureRecord<
+ TProcedures[TKey]['_def']['record'],
+ `${TPath}${TKey & string}.`
+ >
+ : TProcedures[TKey] extends AnyProcedure
+ ? DecorateProcedure
+ : never;
+}
+
+export default defineNuxtPlugin(() => {
+ const client = createTRPCProxyClient({
+ links: [
+ httpBatchLink({
+ url: 'http://localhost:3000/trpc',
+ }),
+ ],
+ })
+
+ const newClient = createFlatProxy((key) => {
+ return createNuxtProxyDecoration(key, client)
+ }) as DecoratedProcedureRecord
+
+ return {
+ provide: {
+ client: newClient,
+ },
+ }
+})
diff --git a/playground/server/trpc/index.ts b/playground/server/trpc/index.ts
index 90a81bd..74d14e5 100644
--- a/playground/server/trpc/index.ts
+++ b/playground/server/trpc/index.ts
@@ -19,13 +19,10 @@ const t = initTRPC.context().create()
// We explicitly export the methods we use here
// This allows us to create reusable & protected base procedures
export const middleware = t.middleware
-export const router = t.router
+const router = t.router
export const publicProcedure = t.procedure
-export const appRouter = router({
- getTodos: publicProcedure.query(() => {
- return $fetch(`${baseURL}/todos`)
- }),
+const anotherRouter = router({
getTodo: publicProcedure
.input(z.number())
.query((req) => {
@@ -41,6 +38,19 @@ export const appRouter = router({
}),
})
+export const appRouter = router({
+ todo: anotherRouter,
+ getTodos: publicProcedure.query(() => {
+ return $fetch(`${baseURL}/todos`)
+ }),
+ getTodo: publicProcedure
+ .input(z.number())
+ .query((req) => {
+ console.log('REQ', req)
+ return $fetch(`${baseURL}/todos/${req.input}`)
+ }),
+})
+
export type AppRouter = typeof appRouter
export async function createContext(event: H3Event) {
diff --git a/src/module.ts b/src/module.ts
index 2df4a06..4fee9f9 100644
--- a/src/module.ts
+++ b/src/module.ts
@@ -3,7 +3,7 @@ import { join, resolve } from 'pathe'
import { defu } from 'defu'
import dedent from 'dedent'
-import { addImports, addPlugin, addServerHandler, addTemplate, defineNuxtModule } from '@nuxt/kit'
+import { addPlugin, addServerHandler, addTemplate, defineNuxtModule } from '@nuxt/kit'
export interface ModuleOptions {
baseURL: string
@@ -32,19 +32,19 @@ export default defineNuxtModule({
endpoint: options.endpoint,
})
- addImports([
- { name: 'useClient', from: join(runtimeDir, 'client') },
- { name: 'useAsyncQuery', from: join(runtimeDir, 'client') },
- { name: 'useClientHeaders', from: join(runtimeDir, 'client') },
- { name: 'getQueryKey', from: join(runtimeDir, 'client') },
- ])
+ // addImports([
+ // { name: 'useClient', from: join(runtimeDir, 'client') },
+ // { name: 'useAsyncQuery', from: join(runtimeDir, 'client') },
+ // { name: 'useClientHeaders', from: join(runtimeDir, 'client') },
+ // { name: 'getQueryKey', from: join(runtimeDir, 'client') },
+ // ])
addServerHandler({
route: `${finalConfig.endpoint}/*`,
handler: handlerPath,
})
- addPlugin(resolve(runtimeDir, 'plugin'))
+ // addPlugin(resolve(runtimeDir, 'plugin'))
addTemplate({
filename: 'trpc-handler.ts',
@@ -56,6 +56,7 @@ export default defineNuxtModule({
export default createTRPCHandler({
...functions,
+ router: functions.appRouter,
endpoint: '${finalConfig.endpoint}'
})
`
diff --git a/src/runtime/api.ts b/src/runtime/api.ts
index d500cb1..e07ef7f 100644
--- a/src/runtime/api.ts
+++ b/src/runtime/api.ts
@@ -11,6 +11,9 @@ import { createURL } from 'ufo'
import type { H3Event } from 'h3'
import { defineEventHandler, isMethod, readBody } from 'h3'
import type { TRPCResponse } from '@trpc/server/rpc'
+import type { CreateTRPCClientOptions, inferRouterProxyClient } from '@trpc/client'
+import { createTRPCProxyClient } from '@trpc/client'
+import { toRaw } from 'vue'
type MaybePromise = T | Promise
@@ -88,3 +91,26 @@ export function createTRPCHandler({
return body
})
}
+
+export function createTRPCNuxtClient(opts: CreateTRPCClientOptions) {
+ const client = createTRPCProxyClient(opts)
+
+ // Object.keys(client).forEach((path) => {
+ // clientWithOther[path] = {}
+ // Object.keys(client[path]).forEach((action) => {
+ // clientWithOther[path][action] = (input: inferRouterInputs) => {
+ // // @ts-expect-error: asd
+ // return useAsyncData(`${path}-${action}`, () => client[path][action](input))
+ // }
+ // })
+ // })
+
+ const proxiedClient = new Proxy({}, {
+ get(target, property) {
+ // @ts-expect-error: Nuxt
+ return () => useAsyncData(`${target}-${property}`, () => client.getTodos.query())
+ },
+ })
+
+ return proxiedClient as inferRouterProxyClient
+}
diff --git a/src/runtime/client.ts b/src/runtime/client.ts
index 4cdd3c5..ae3d165 100644
--- a/src/runtime/client.ts
+++ b/src/runtime/client.ts
@@ -1,3 +1,15 @@
+import type { CreateTRPCClientOptions, TRPCClientErrorLike, inferRouterProxyClient } from '@trpc/client'
+import { createTRPCProxyClient } from '@trpc/client'
+import type {
+ AnyMutationProcedure,
+ AnyProcedure,
+ AnyQueryProcedure,
+ AnyRouter,
+ ProcedureRouterRecord,
+ inferProcedureInput,
+ inferProcedureOutput,
+} from '@trpc/server'
+import { createFlatProxy, createRecursiveProxy } from '@trpc/server/shared'
import type {
AsyncData,
AsyncDataOptions,
@@ -5,80 +17,97 @@ import type {
PickFrom,
_Transform,
} from 'nuxt/dist/app/composables/asyncData'
-import type {
- ProcedureRecord,
- inferHandlerInput,
- inferProcedureInput,
- inferProcedureOutput,
-} from '@trpc/server'
-import type { TRPCClient, TRPCClientErrorLike } from '@trpc/client'
-import { objectHash } from 'ohash'
-import type { Ref } from 'vue'
-import { useAsyncData, useNuxtApp, useState } from '#app'
-import type { AppRouter } from '~/server/trpc'
-
-type MaybeRef = T | Ref
-
-export type inferProcedures<
- TObj extends ProcedureRecord,
-> = {
- [TPath in keyof TObj]: {
- input: inferProcedureInput
- output: inferProcedureOutput
- };
-}
-
-export type TQueries = AppRouter['_def']['procedures']
-export type TError = TRPCClientErrorLike
-
-export type TQueryValues = inferProcedures
+import { hash } from 'ohash'
/**
* Calculates the key used for `useAsyncData` call
- * @param pathAndInput
*/
-export function getQueryKey<
- TPath extends keyof TQueryValues & string,
- >(pathAndInput: [path: TPath, ...args: inferHandlerInput]) {
- return `${pathAndInput[0]}-${objectHash(pathAndInput[1] ? JSON.stringify(pathAndInput[1]) : '')}`
+export function getQueryKey(
+ path: string,
+ input: unknown,
+): string {
+ return input === undefined ? path : `${path}-${hash(input || '')}`
}
-export async function useAsyncQuery<
- TPath extends keyof TQueryValues & string,
- TOutput extends TQueryValues[TPath]['output'] = TQueryValues[TPath]['output'],
- Transform extends _Transform = _Transform,
- PickKeys extends KeyOfRes = KeyOfRes,
->(
- pathAndInput: [path: TPath, ...args: inferHandlerInput],
- options: AsyncDataOptions = {},
-): Promise, PickKeys>, TError>> {
- const { $client } = useNuxtApp()
- const key = getQueryKey(pathAndInput)
- const serverError = useState(`error-${key}`, () => null)
- const { error, data, ...rest } = await useAsyncData(
- key,
- () => $client.query(...pathAndInput),
- options,
- )
+function createNuxtProxyDecoration(name: string, client: inferRouterProxyClient) {
+ return createRecursiveProxy((opts) => {
+ const args = opts.args
- if (error.value && !serverError.value)
- serverError.value = error.value as any
+ const pathCopy = [name, ...opts.path]
- if (data.value)
- serverError.value = null
+ // The last arg is for instance `.mutate` or `.query()`
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const lastArg = pathCopy.pop()!
- return {
- ...rest,
- data,
- error: serverError,
- } as any
+ const path = pathCopy.join('.')
+
+ const [input, asyncDataOptions] = args
+
+ const queryKey = getQueryKey(path, input)
+
+ if (lastArg === 'mutate') {
+ // @ts-expect-error: Nuxt internal
+ return useAsyncData(queryKey, () => (client as any)[path][lastArg](input), {
+ ...asyncDataOptions as Record,
+ immediate: false,
+ })
+ }
+
+ // @ts-expect-error: Nuxt internal
+ return useAsyncData(queryKey, () => (client as any)[path][lastArg](input), asyncDataOptions as Record)
+ })
}
-export function useClient(): TRPCClient {
- const { $client } = useNuxtApp()
- return $client
+/**
+ * @internal
+ */
+export type DecorateProcedure<
+ TProcedure extends AnyProcedure,
+ TPath extends string,
+> = TProcedure extends AnyQueryProcedure
+ ? {
+ query: <
+ TData = inferProcedureOutput,
+ Transform extends _Transform = _Transform,
+ PickKeys extends KeyOfRes = KeyOfRes,
+ >(
+ input: inferProcedureInput,
+ opts?: AsyncDataOptions,
+ ) => AsyncData, PickKeys>, TRPCClientErrorLike>
+ } : TProcedure extends AnyMutationProcedure ? {
+ mutate: <
+ TData = inferProcedureOutput,
+ Transform extends _Transform = _Transform,
+ PickKeys extends KeyOfRes = KeyOfRes,
+ >(
+ input: inferProcedureInput,
+ opts?: AsyncDataOptions,
+ ) => AsyncData, PickKeys>, TRPCClientErrorLike>
+ } : never
+
+/**
+* @internal
+*/
+export type DecoratedProcedureRecord<
+ TProcedures extends ProcedureRouterRecord,
+ TPath extends string = '',
+> = {
+ [TKey in keyof TProcedures]: TProcedures[TKey] extends AnyRouter
+ ? DecoratedProcedureRecord<
+ TProcedures[TKey]['_def']['record'],
+ `${TPath}${TKey & string}.`
+ >
+ : TProcedures[TKey] extends AnyProcedure
+ ? DecorateProcedure
+ : never;
}
-export function useClientHeaders(initialValue: MaybeRef> = {}): Ref> {
- return useState('trpc-nuxt-header', () => initialValue)
+export function createTRPCNuxtProxyClient(opts: CreateTRPCClientOptions) {
+ const client = createTRPCProxyClient(opts)
+
+ const decoratedClient = createFlatProxy((key) => {
+ return createNuxtProxyDecoration(key, client)
+ }) as DecoratedProcedureRecord
+
+ return decoratedClient
}
diff --git a/src/runtime/plugin.ts b/src/runtime/plugin.ts
index 045008f..393f003 100644
--- a/src/runtime/plugin.ts
+++ b/src/runtime/plugin.ts
@@ -1,5 +1,5 @@
+import type { inferRouterProxyClient } from '@trpc/client'
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'
-import type { TRPCClient } from '@trpc/client';
import { unref } from 'vue'
import { FetchError } from 'ohmyfetch'
import { useClientHeaders } from './client'
@@ -12,6 +12,7 @@ export default defineNuxtPlugin((nuxtApp) => {
const otherHeaders = useClientHeaders()
const baseURL = process.server ? '' : config.baseURL
+
const client = createTRPCProxyClient({
links: [
httpBatchLink({
@@ -43,6 +44,6 @@ export default defineNuxtPlugin((nuxtApp) => {
declare module '#app' {
interface NuxtApp {
- $client: TRPCClient
+ $client: inferRouterProxyClient
}
}
diff --git a/test.js b/test.js
new file mode 100644
index 0000000..8ebf707
--- /dev/null
+++ b/test.js
@@ -0,0 +1,23 @@
+const data = {
+ hello: {
+ log() {
+ return 'hello log';
+ },
+ },
+ hi: {
+ log() {
+ return 'hi log'
+ },
+ },
+}
+
+const blankObject = {};
+
+const proxy = new Proxy(blankObject, {
+ get(target, key) {
+ if (key in data) return data[key]; // <---
+ return target[key]; // default
+ }
+});
+
+console.log(proxy.hello.log()); // hello log;