diff --git a/README.md b/README.md index 68f4267..03e9bde 100644 --- a/README.md +++ b/README.md @@ -52,33 +52,34 @@ export const router = trpc Use the client like so: -```html - +const farewell = await client.query('bye') +console.log(farewell) // => 👈 goodbye ``` -## `useTRPCAsyncData` +## useAsyncQuery -A composable that wraps Nuxt's [`useAsyncData`](https://v3.nuxtjs.org/api/composables/use-async-data/) with some modifications to have better error handlings. +A thin wrapper around [`useAsyncData`](https://v3.nuxtjs.org/api/composables/use-async-data/). + +The first argument is a `[path, input]`-tuple - if the `input` is optional, you can omit the, `input`-part. + +You'll notice that you get autocompletion on the `path` and automatic typesafety on the `input`. ```ts -const path = 'hello' -const client = useClient() - const { data, pending, error, refresh -} = await useTRPCAsyncData(path, () => client.query(path)) -console.log(data.value) // => 👈 world +} = await useAsyncQuery(['getUser', { id: 69 }], { + // pass useAsyncData options here + server: true +}) ``` ## Recipes @@ -86,6 +87,7 @@ console.log(data.value) // => 👈 world - [Validation](/recipes/validation.md) - [Authorization](/recipes/authorization.md) - [Error Handling](/recipes/error-handling.md) +- [Error Formatting](/recipes/error-formatting.md) Learn more about tRPC.io [here](https://trpc.io/docs). diff --git a/playground/app.vue b/playground/app.vue index 663fc3d..943c67f 100644 --- a/playground/app.vue +++ b/playground/app.vue @@ -1,11 +1,9 @@ diff --git a/playground/server/trpc/index.ts b/playground/server/trpc/index.ts index 434830f..dead09a 100644 --- a/playground/server/trpc/index.ts +++ b/playground/server/trpc/index.ts @@ -14,6 +14,19 @@ const fakeUsers = [ export const router = trpc .router>() + .formatError(({ shape, error }) => { + return { + ...shape, + data: { + ...shape.data, + zodError: + error.code === 'BAD_REQUEST' + && error.cause instanceof ZodError + ? error.cause.flatten() + : null, + }, + } + }) .query('getUsers', { resolve() { return fakeUsers diff --git a/recipes/error-formatting.md b/recipes/error-formatting.md new file mode 100644 index 0000000..176dea7 --- /dev/null +++ b/recipes/error-formatting.md @@ -0,0 +1,31 @@ +## Error Formatting + +The error formatting in your router will be inferred all the way to your client (& Vue components). + +### Adding custom formatting + +```ts +// ~/server/trpc/index.ts +import * as trpc from '@trpc/server' + +export const router = trpc.router() + .formatError(({ shape, error }) => { + return { + ...shape, + data: { + ...shape.data, + zodError: + error.code === 'BAD_USER_INPUT' + && error.cause instanceof ZodError + ? error.cause.flatten() + : null, + } + } + }) +``` + +### Usage in Vue + +```html + +``` diff --git a/src/module.ts b/src/module.ts index 8c002f6..da4cca0 100644 --- a/src/module.ts +++ b/src/module.ts @@ -33,7 +33,7 @@ export default defineNuxtModule({ nuxt.hook('autoImports:extend', (imports) => { imports.push( { name: 'useClient', from: clientPath }, - { name: 'useTRPCAsyncData', from: join(runtimeDir, 'composables') }, + { name: 'useAsyncQuery', from: join(runtimeDir, 'client') }, ) }) diff --git a/src/runtime/client.ts b/src/runtime/client.ts new file mode 100644 index 0000000..742e680 --- /dev/null +++ b/src/runtime/client.ts @@ -0,0 +1,63 @@ +import type { + AsyncData, + AsyncDataOptions, + KeyOfRes, + PickFrom, + _Transform, +} from 'nuxt/dist/app/composables/asyncData' +import type { ProcedureRecord, inferHandlerInput, inferProcedureInput, inferProcedureOutput } from '@trpc/server' +import type { TRPCClientErrorLike } from '@trpc/client' +import { objectHash } from 'ohash' +// @ts-expect-error: Resolved by Nuxt +import { useAsyncData, useState } from '#imports' +// @ts-expect-error: Resolved by Nuxt +import { useClient } from '#build/trpc-client' +// @ts-expect-error: Resolved by Nuxt +import type { router } from '~/server/trpc' + +type AppRouter = typeof router + +type inferProcedures< + TObj extends ProcedureRecord, +> = { + [TPath in keyof TObj]: { + input: inferProcedureInput + output: inferProcedureOutput + }; +} + +type TQueries = AppRouter['_def']['queries'] +type TError = TRPCClientErrorLike + +type TQueryValues = inferProcedures + +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 = useClient() + const key = `${pathAndInput[0]}-${objectHash(pathAndInput[1] ? JSON.stringify(pathAndInput[1]) : '')}` + const serverError = useState(`error-${key}`, () => null) + const { error, data, ...rest } = await useAsyncData( + key, + () => client.query(...pathAndInput), + options, + ) + + if (process.server && error.value && !serverError.value) + serverError.value = error.value as any + + if (data.value) + serverError.value = null + + return { + ...rest, + data, + error: serverError, + } as any +} diff --git a/src/runtime/composables.ts b/src/runtime/composables.ts deleted file mode 100644 index 9e9bcf1..0000000 --- a/src/runtime/composables.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { - AsyncData, - KeyOfRes, - PickFrom, - _Transform, -} from 'nuxt/dist/app/composables/asyncData' -import type { AsyncDataOptions, NuxtApp } from '#app' -// @ts-expect-error: Resolved by Nuxt -import { useAsyncData, useState } from '#imports' - -export async function useTRPCAsyncData< - DataT, - DataE = Error, - Transform extends _Transform = _Transform, - PickKeys extends KeyOfRes = KeyOfRes, ->( - key: string, - handler: (ctx?: NuxtApp) => Promise, - options: AsyncDataOptions = {}, -): Promise, PickKeys>, DataE | null | true>> { - const serverError = useState(`error-${key}`, () => null) - const { error, data, ...rest } = await useAsyncData(key, handler, options) - - // Only set the value on server and if serverError is empty - if (process.server && error.value && !serverError.value) - serverError.value = error.value as DataE | true | null - - // Clear error if data is available - if (data.value) - serverError.value = null - - return { - ...rest, - data, - error: serverError, - } -}