Compare commits

...

42 Commits

Author SHA1 Message Date
wobsoriano
f061b0531f chore: release v0.4.2 2022-11-17 02:42:20 -08:00
wobsoriano
f08b724df7 feat: get req,res values from event.node 2022-11-17 02:42:01 -08:00
wobsoriano
4085d5fe26 fix: request types 2022-11-17 02:41:01 -08:00
wobsoriano
13713343ce fix: request types 2022-11-17 02:39:45 -08:00
wobsoriano
d6f11bb301 feat(deps): bump h3 to 1.0.1 2022-11-17 02:38:27 -08:00
wobsoriano
c5c1f1cdba feat(deps): bump h3, ohash and ufo to 1.0.0 2022-11-17 02:38:17 -08:00
wobsoriano
0681dd554d Merge branch 'next' of https://github.com/wobsoriano/trpc-nuxt into next 2022-11-16 08:48:14 -08:00
wobsoriano
e7a76694af update docs 2022-11-16 08:48:09 -08:00
wobsoriano
da75c18ebe update local nuxt dep to stable 2022-11-16 08:42:45 -08:00
wobsoriano
6289575900 bump local trpc deps to rc.8 2022-11-16 08:40:35 -08:00
Robert Soriano
ac212672e4 Update 2.server-side-calls.md 2022-11-14 12:26:48 -08:00
Robert Soriano
b8e2a83e26 Merge pull request #42 from nkhdo/patch-1
Update 2.server-side-calls.md
2022-11-14 10:43:06 -08:00
Hoang Do
fd98b0d414 Update 2.server-side-calls.md
Fix wrong variable import
2022-11-15 00:46:06 +07:00
wobsoriano
20ab235856 docs: add auth tips 2022-11-13 11:18:57 -08:00
wobsoriano
7205c356f8 docs: update aborting procs 2022-11-13 11:08:28 -08:00
wobsoriano
e14827e0c7 docs: add aborting procs 2022-11-13 11:05:47 -08:00
wobsoriano
eea6406d5a docs: update example 2022-11-13 10:58:27 -08:00
wobsoriano
2d3409a25b docs: update example 2022-11-13 10:58:08 -08:00
wobsoriano
7a71a6e128 docs: add mutation example 2022-11-13 10:55:23 -08:00
wobsoriano
d8e89297cd docs: typo fix 2022-11-13 10:41:35 -08:00
wobsoriano
6768205318 docs: update composables 2022-11-13 10:41:22 -08:00
wobsoriano
c20a092a31 docs: add tips section 2022-11-13 10:37:24 -08:00
wobsoriano
64df8fb7ad infer error in playground 2022-11-13 09:43:23 -08:00
wobsoriano
d5567e5826 remove character 2022-11-12 21:16:19 -08:00
wobsoriano
6b898e836c update readme 2022-11-12 21:15:19 -08:00
wobsoriano
fd38c4f983 chore: release v0.4.1 2022-11-12 20:16:38 -08:00
wobsoriano
48d4c6342f update playground 2022-11-12 20:16:29 -08:00
wobsoriano
297c3d52f4 docs: update usage 2022-11-12 20:15:52 -08:00
wobsoriano
0f9653d1a4 remove /module 2022-11-12 20:12:36 -08:00
wobsoriano
85998101c7 feat: remove nuxt 3 wrappers for client 2022-11-12 20:10:50 -08:00
wobsoriano
d6cb770154 update installation instructions 2022-11-12 18:41:44 -08:00
wobsoriano
c222008524 update installation instructions 2022-11-12 18:41:16 -08:00
wobsoriano
9cd21eb300 update readme docs url 2022-11-12 18:40:15 -08:00
wobsoriano
b4e83dbb0b add back preset 2022-11-12 18:38:25 -08:00
wobsoriano
0fa63c86ba test node preset 2022-11-12 18:31:35 -08:00
wobsoriano
7371dc4c10 update docs 2022-11-12 18:28:26 -08:00
wobsoriano
77ab5f0dc6 reinstall deps 2022-11-12 18:20:07 -08:00
wobsoriano
ba15d15d67 remove patched @vueuse/head 2022-11-12 18:17:10 -08:00
wobsoriano
7999039088 update github config 2022-11-12 18:13:49 -08:00
wobsoriano
dc15500074 dont prerender index 2022-11-12 18:13:00 -08:00
wobsoriano
99a83562aa add tmp docs link 2022-11-12 18:09:34 -08:00
wobsoriano
e04344d508 docs: remove package manager 2022-11-12 18:08:22 -08:00
28 changed files with 1279 additions and 1672 deletions

View File

@@ -2,6 +2,17 @@
End-to-end typesafe APIs with [tRPC.io](https://trpc.io/) in Nuxt applications. End-to-end typesafe APIs with [tRPC.io](https://trpc.io/) in Nuxt applications.
<p align="center">
<figure>
<img src="https://i.imgur.com/3AZlBZH.gif" alt="Demo" />
<figcaption>
<p align="center">
The client above is <strong>not</strong> importing any code from the server, only its type declarations.
</p>
</figcaption>
</figure>
</p>
Docs: https://trpc-nuxt.vercel.app Docs: https://trpc-nuxt.vercel.app
For version 3 of this module (tRPC v9, auto-imports, auto handlers), [go here](https://github.com/wobsoriano/trpc-nuxt/tree/v3). For version 3 of this module (tRPC v9, auto-imports, auto handlers), [go here](https://github.com/wobsoriano/trpc-nuxt/tree/v3).

1
client.d.ts vendored
View File

@@ -1 +0,0 @@
export * from './dist/client/index'

View File

@@ -1,5 +1,25 @@
export default defineAppConfig({ export default defineAppConfig({
docus: { docus: {
title: 'tRPC Nuxt' title: 'tRPC Nuxt',
image: 'https://og-image.vercel.app/tRPC-Nuxt',
alt: 'tRPC-Nuxt cover',
url: 'https://trpc-nuxt.vercel.app',
debug: false,
socials: {
github: 'wobsoriano/trpc-nuxt'
},
aside: {
level: 1
},
footer: {
credits: true,
icons: [
{
label: 'NuxtJS',
href: 'https://nuxtjs.org',
component: 'IconNuxt'
}
]
}
} }
}) })

View File

@@ -5,20 +5,18 @@ description: tRPC-Nuxt provides first class integration with tRPC.
# Installation # Installation
## 1. Add to existing Nuxt project
::code-group ::code-group
```bash [pnpm] ```bash [pnpm]
pnpm add @trpc/server@next @trpc/client@next trpc-nuxt@beta zod pnpm add @trpc/server@next @trpc/client@next trpc-nuxt zod
``` ```
```bash [npm] ```bash [npm]
npm install @trpc/server@next @trpc/client@next trpc-nuxt@beta zod npm install @trpc/server@next @trpc/client@next trpc-nuxt zod
``` ```
```bash [yarn] ```bash [yarn]
yarn add @trpc/server@next @trpc/client@next trpc-nuxt@beta zod yarn add @trpc/server@next @trpc/client@next trpc-nuxt zod
``` ```
:: ::
@@ -35,18 +33,6 @@ For making typesafe API calls from your client.
Most examples use [Zod](https://github.com/colinhacks/zod) for input validation and tRPC.io highly recommends it, though it isn't required. Most examples use [Zod](https://github.com/colinhacks/zod) for input validation and tRPC.io highly recommends it, though it isn't required.
## 2. Install module
This will transpile `trpc-nuxt` and exclude `trpc-nuxt/client` from optimized dependencies by Vite.
```ts [nuxt.config.ts]
export default defineNuxtConfig({
modules: ['trpc-nuxt/module']
})
```
::
## Next Steps ## Next Steps
Now that you've installed the required dependencies, you are ready to start building your application. Now that you've installed the required dependencies, you are ready to start building your application.

View File

@@ -88,15 +88,14 @@ If you need to split your router into several subrouters, you can implement them
## 2. Create tRPC client plugin ## 2. Create tRPC client plugin
Create a set of strongly-typed composables using your API's type signature. Create a strongly-typed plugin using your API's type signature.
```ts [plugins/client.ts] ```ts [plugins/client.ts]
import { httpBatchLink } from '@trpc/client' import { httpBatchLink, createTRPCProxyClient } from '@trpc/client'
import { createTRPCNuxtProxyClient } from 'trpc-nuxt/client'
import type { AppRouter } from '@/server/trpc/routers' import type { AppRouter } from '@/server/trpc/routers'
export default defineNuxtPlugin(() => { export default defineNuxtPlugin(() => {
const client = createTRPCNuxtProxyClient<AppRouter>({ const client = createTRPCProxyClient<AppRouter>({
links: [ links: [
httpBatchLink({ httpBatchLink({
/** /**
@@ -122,19 +121,10 @@ export default defineNuxtPlugin(() => {
<script setup lang="ts"> <script setup lang="ts">
const { $client } = useNuxtApp() const { $client } = useNuxtApp()
// query and mutate uses useAsyncData under the hood const data = await $client.hello.query({ text: 'client' })
const { data, pending, error } = await $client.hello.query({ text: 'client' })
</script> </script>
<template> <template>
<div v-if="pending"> <p>{{data?.greeting }}</p>
Loading...
</div>
<div v-else-if="error?.data?.code">
Error: {{ error.data.code }}
</div>
<div v-else>
<p>{{ hello.data?.greeting }}</p>
</div>
</template> </template>
``` ```

View File

@@ -0,0 +1,40 @@
---
title: Composables
---
# Composables
It is often useful to wrap functionality of your `@trpc/client` api within other functions. For this purpose, it's necessary to be able to infer input types and output types generated by your `@trpc/server` router.
## Inference Helpers
`@trpc/server` exports the following helper types to assist with inferring these types from the `AppRouter` exported by your `@trpc/server` router:
- `inferRouterInputs<TRouter>`
- `inferRouterOutputs<TRouter>`
Let's assume we have this example query wrapped within Nuxt's [useAsyncData](https://v3.nuxtjs.org/api/composables/use-async-data/):
```ts
const { data, error } = await useAsyncData(() => $client.todo.getTodos.query())
```
We can wrap this in a composable and also set the client error types:
```ts [composables/useGetTodos.ts]
import { TRPCClientError } from '@trpc/client'
import type { inferRouterOutputs } from '@trpc/server'
import type { AppRouter } from '@/server/trpc/routers'
type RouterOutput = inferRouterOutputs<AppRouter>
type GetTodosOutput = RouterOutput['todo']['getTodos']
type ErrorOutput = TRPCClientError<AppRouter>
export default function useGetTodos() {
const { $client } = useNuxtApp()
return useAsyncData<GetTodosOutput, ErrorOutput>(() => $client.todo.getTodos.query())
}
```
Now, we have a fully-typed composable.

View File

@@ -0,0 +1,84 @@
---
title: Server Side Calls
---
# Server Side Calls
You may need to call your procedure(s) directly from the server, `createCaller()` function returns you an instance of `RouterCaller` able to execute queries and mutations.
## Input query example
We create the router with a input query and then we call the asynchronous `greeting` procedure to get the result.
::code-group
```ts [server/trpc/trpc.ts]
import { initTRPC } from '@trpc/server'
import { z } from 'zod'
const t = initTRPC.create()
export const router = t.router({
// Create procedure at path 'greeting'
greeting: t.procedure
.input(z.object({ name: z.string() }))
.query(({ input }) => `Hello ${input.name}`),
})
```
```ts [server/api/greeting.ts]
import { router } from '@/server/trpc/trpc'
export default eventHandler(async (event) => {
const { name } = getQuery(event)
const caller = router.createCaller({})
const greeting = await caller.greeting({ name })
return {
greeting
}
})
```
::
## Mutation example
We create the router with a mutation and then we call the asynchronous `post` procedure to get the result.
::code-group
```ts [server/trpc/trpc.ts]
import { initTRPC } from '@trpc/server'
import { z } from 'zod'
const posts = ['One', 'Two', 'Three']
const t = initTRPC.create()
export const router = t.router({
post: t.router({
add: t.procedure.input(z.string()).mutation(({ input }) => {
posts.push(input)
return posts
}),
}),
})
```
```ts [server/api/post.ts]
import { router } from '@/server/trpc/trpc'
export default eventHandler(async (event) => {
const body = await getBody(event)
const caller = router.createCaller({})
const post = await caller.post.add(body.post)
return {
post
}
})
```
::

View File

@@ -0,0 +1,24 @@
---
title: Aborting Procedures
---
# Aborting Procedures
tRPC adheres to the industry standard when it comes to aborting procedures. All you have to do is pass an `AbortSignal` to the query-options and then call its parent `AbortController`'s `abort` method.
```ts [composables/useGetTodo.ts]
export default function useGetTodo(id: number) {
const { $client } = useNuxtApp()
const ac = new AbortController()
onScopeDispose(() => {
ac.abort()
})
return useAsyncData(() => {
return $client.todo.getTodo.query(id, {
signal: ac.signal
})
})
}
```

View File

@@ -0,0 +1,104 @@
---
title: Authorization
---
# Authorization
The `createContext` function is called for each incoming request so here you can add contextual information about the calling user from the request object.
## Create context from request headers
```ts [server/trpc/context.ts]
import type { H3Event } from 'h3'
import { inferAsyncReturnType } from '@trpc/server'
import { decodeAndVerifyJwtToken } from './somewhere/in/your/app/utils'
export async function createContext({
req,
res,
}: H3Event) {
// Create your context based on the request object
// Will be available as `ctx` in all your resolvers
// This is just an example of something you might want to do in your ctx fn
async function getUserFromHeader() {
if (req.headers.authorization) {
const user = await decodeAndVerifyJwtToken(
req.headers.authorization.split(' ')[1],
)
return user
}
return null
}
const user = await getUserFromHeader()
return {
user,
}
}
type Context = inferAsyncReturnType<typeof createContext>
```
## Option 1: Authorize using resolver
```ts
import { TRPCError, initTRPC } from '@trpc/server'
import type { Context } from '../context'
export const t = initTRPC.context<Context>().create()
const appRouter = t.router({
// open for anyone
hello: t.procedure
.input(z.string().nullish())
.query(({ input, ctx }) => `hello ${input ?? ctx.user?.name ?? 'world'}`),
// checked in resolver
secret: t.procedure.query(({ ctx }) => {
if (!ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' })
}
return {
secret: 'sauce',
}
}),
})
```
## Option 2: Authorize using middleware
```ts
import { TRPCError, initTRPC } from '@trpc/server'
export const t = initTRPC.context<Context>().create()
const isAuthed = t.middleware(({ next, ctx }) => {
if (!ctx.user?.isAdmin) {
throw new TRPCError({ code: 'UNAUTHORIZED' })
}
return next({
ctx: {
user: ctx.user,
},
})
})
// you can reuse this for any procedure
export const protectedProcedure = t.procedure.use(isAuthed)
t.router({
// this is accessible for everyone
hello: t.procedure
.input(z.string().nullish())
.query(({ input, ctx }) => `hello ${input ?? ctx.user?.name ?? 'world'}`),
admin: t.router({
// this is accessible only to admins
secret: protectedProcedure.query(({ ctx }) => {
return {
secret: 'sauce',
}
}),
}),
})
```
This page is entirely based on [authorization docs](https://trpc.io/docs/v10/authorization) of tRPC with a minimal change made to work with Nuxt.

View File

@@ -0,0 +1,4 @@
---
navigation: false
redirect: /get-started/installation
---

View File

@@ -1,6 +1,6 @@
--- ---
title: "tRPC Nuxt" title: "tRPC Nuxt"
description: "A supa simple wrapper arousnd supabase-js to enable usage and integration within Nuxt." description: "End-to-end typesafe APIs in Nuxt applications."
navigation: false navigation: false
layout: page layout: page
--- ---
@@ -13,7 +13,7 @@ cta:
secondary: secondary:
- Star on GitHub -> - Star on GitHub ->
- https://github.com/wobsoriano/trpc-nuxt - https://github.com/wobsoriano/trpc-nuxt
snippet: npm install trpc-nuxt@beta snippet: npm install trpc-nuxt
--- ---
#title #title
@@ -24,8 +24,8 @@ End-to-end typesafe APIs in Nuxt applications.
#extra #extra
::list ::list
- Automatic typesafety - Automatic typesafety
- Snappy DX - Snappy DX
- Autocompletion - Autocompletion on the client, for inputs, outputs and errors
:: ::
:: ::

View File

@@ -6,9 +6,9 @@ export default defineNuxtConfig({
modules: ['@nuxtlabs/github-module'], modules: ['@nuxtlabs/github-module'],
extends: process.env.DOCUS_THEME_PATH || '@nuxt-themes/docus', extends: process.env.DOCUS_THEME_PATH || '@nuxt-themes/docus',
github: { github: {
owner: 'nuxt', owner: 'wobsoriano',
repo: 'content', repo: 'trpc-nuxt',
branch: 'main' branch: 'next'
}, },
colorMode: { colorMode: {
preference: 'dark' preference: 'dark'
@@ -17,10 +17,6 @@ export default defineNuxtConfig({
transpile: [/content-edge/, /github-module/] transpile: [/content-edge/, /github-module/]
}, },
nitro: { nitro: {
prerender: {
crawlLinks: true,
routes: ['/']
},
preset: 'vercel' preset: 'vercel'
} }
}) })

View File

@@ -8,12 +8,10 @@
"preview": "nuxi preview" "preview": "nuxi preview"
}, },
"dependencies": { "dependencies": {
"nuxt": "^3.0.0-rc.13", "nuxt": "^3.0.0"
"@nuxtjs/tailwindcss": "^6.1.3"
}, },
"devDependencies": { "devDependencies": {
"@nuxt-themes/docus": "npm:@nuxt-themes/docus-edge@latest", "@nuxt-themes/docus": "^0.3.1",
"@nuxtlabs/github-module": "npm:@nuxtlabs/github-module-edge@latest" "@nuxtlabs/github-module": "^1.5.3"
}, }
"packageManager": "yarn@1.22.19"
} }

View File

@@ -1,14 +1,6 @@
import { defineTheme } from 'pinceau' import { defineTheme } from 'pinceau'
export default defineTheme({ export default defineTheme({
title: 'tRPC-Nuxt',
cover: {
src: 'https://og-image.vercel.app/tRPC-Nuxt',
alt: 'tRPC-Nuxt conver'
},
aside: {
level: 1
},
colors: { colors: {
primary: { primary: {
50: '#BFEDFC', 50: '#BFEDFC',

View File

@@ -1,17 +0,0 @@
import { defineNuxtModule } from '@nuxt/kit'
export default defineNuxtModule({
meta: {
name: 'trpc-nuxt',
configKey: 'trpc',
compatibility: {
nuxt: '^3.0.0-rc.13'
}
},
setup (_moduleOptions, nuxt) {
nuxt.options.build.transpile.push('trpc-nuxt')
nuxt.options.vite.optimizeDeps = nuxt.options.vite.optimizeDeps || {}
nuxt.options.vite.optimizeDeps.exclude = nuxt.options.vite.optimizeDeps.exclude || []
nuxt.options.vite.optimizeDeps.exclude.push('trpc-nuxt/client')
}
})

View File

@@ -1,30 +1,21 @@
{ {
"name": "trpc-nuxt", "name": "trpc-nuxt",
"description": "End-to-end typesafe APIs in Nuxt applications.",
"type": "module", "type": "module",
"version": "0.4.0", "version": "0.4.2",
"license": "MIT", "license": "MIT",
"sideEffects": false, "sideEffects": false,
"exports": { "exports": {
"./package.json": "./package.json",
".": { ".": {
"require": "./dist/index.cjs", "require": "./dist/index.cjs",
"import": "./dist/index.mjs" "import": "./dist/index.mjs"
},
"./client": {
"require": "./dist/client/index.cjs",
"import": "./dist/client/index.mjs"
},
"./module": {
"import": "./module.mjs"
} }
}, },
"main": "./dist/index.mjs", "main": "./dist/index.mjs",
"module": "./dist/index.mjs", "module": "./dist/index.mjs",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"files": [ "files": [
"dist", "dist"
"client.d.ts",
"module.mjs"
], ],
"scripts": { "scripts": {
"dev": "concurrently \"pnpm build --watch\" \"pnpm --filter playground dev\"", "dev": "concurrently \"pnpm build --watch\" \"pnpm --filter playground dev\"",
@@ -40,28 +31,21 @@
"@trpc/server": "^10.0.0-proxy-beta.21" "@trpc/server": "^10.0.0-proxy-beta.21"
}, },
"dependencies": { "dependencies": {
"h3": "^0.8.6", "h3": "^1.0.1",
"nanoid": "^4.0.0", "nanoid": "^4.0.0",
"nuxt": "^3.0.0-rc.13", "ohash": "^1.0.0",
"ohash": "^0.1.5", "ufo": "^1.0.0"
"ufo": "^0.8.6"
}, },
"devDependencies": { "devDependencies": {
"@nuxt/kit": "3.0.0-rc.13",
"@nuxtjs/eslint-config-typescript": "^11.0.0", "@nuxtjs/eslint-config-typescript": "^11.0.0",
"@trpc/client": "10.0.0-rc.7", "@trpc/client": "10.0.0-rc.8",
"@trpc/server": "10.0.0-rc.7", "@trpc/server": "10.0.0-rc.8",
"bumpp": "^8.2.1", "bumpp": "^8.2.1",
"concurrently": "^7.5.0", "concurrently": "^7.5.0",
"eslint": "^8.25.0", "eslint": "^8.25.0",
"tsup": "6.4.0", "tsup": "6.4.0",
"typescript": "^4.7.4" "typescript": "^4.7.4"
}, },
"pnpm": {
"patchedDependencies": {
"nuxt@3.0.0-rc.13": "patches/nuxt@3.0.0-rc.13.patch"
}
},
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
"@nuxtjs/eslint-config-typescript" "@nuxtjs/eslint-config-typescript"
@@ -77,6 +61,8 @@
"eslintIgnore": [ "eslintIgnore": [
"*.json", "*.json",
"node_modules", "node_modules",
"*.md" "*.md",
"dist",
".output"
] ]
} }

View File

@@ -1,22 +0,0 @@
diff --git a/dist/head/runtime/lib/vueuse-head.plugin.mjs b/dist/head/runtime/lib/vueuse-head.plugin.mjs
index a3278e56986d8e8efb099c502c82d9661a36d846..148a8879ba433072cecd15995cfd5b1b69941124 100644
--- a/dist/head/runtime/lib/vueuse-head.plugin.mjs
+++ b/dist/head/runtime/lib/vueuse-head.plugin.mjs
@@ -4,7 +4,7 @@ import { defineNuxtPlugin, useRouter } from "#app";
import { appHead } from "#build/nuxt.config.mjs";
export default defineNuxtPlugin((nuxtApp) => {
const head = createHead();
- head.addEntry(appHead, { resolved: true });
+ head.addHeadObjs(appHead, { resolved: true });
nuxtApp.vueApp.use(head);
if (process.client) {
let pauseDOMUpdates = true;
@@ -25,7 +25,7 @@ export default defineNuxtPlugin((nuxtApp) => {
}
nuxtApp._useHead = (_meta, options) => {
if (process.server) {
- head.addEntry(_meta, options);
+ head.addHeadObjs(_meta, options);
return;
}
const cleanUp = head.addReactiveEntry(_meta, options);

View File

@@ -1,4 +1,3 @@
// https://v3.nuxtjs.org/api/configuration/nuxt.config // https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({ export default defineNuxtConfig({
modules: ['trpc-nuxt/module']
}) })

View File

@@ -9,13 +9,13 @@
"postinstall": "nuxt prepare" "postinstall": "nuxt prepare"
}, },
"dependencies": { "dependencies": {
"@trpc/client": "10.0.0-rc.7", "@trpc/client": "10.0.0-rc.8",
"@trpc/server": "10.0.0-rc.7", "@trpc/server": "10.0.0-rc.8",
"superjson": "^1.11.0", "superjson": "^1.11.0",
"trpc-nuxt": "workspace:*", "trpc-nuxt": "workspace:*",
"zod": "^3.19.1" "zod": "^3.19.1"
}, },
"devDependencies": { "devDependencies": {
"nuxt": "^3.0.0-rc.13" "nuxt": "^3.0.0"
} }
} }

View File

@@ -1,4 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { TRPCClientError } from '@trpc/client';
import type { inferRouterOutputs } from '@trpc/server';
import type { AppRouter } from '~~/server/trpc/routers';
const { $client } = useNuxtApp() const { $client } = useNuxtApp()
// const headers = useClientHeaders() // const headers = useClientHeaders()
@@ -17,17 +21,16 @@ const addTodo = async () => {
title, title,
completed: false completed: false
}) })
console.log(x.data.value) console.log(x)
} catch (e) { } catch (e) {
console.log(e) console.log(e)
} }
} }
const { data: todos, pending, error, refresh } = await $client.todo.getTodos.query(undefined, { type RouterOutput = inferRouterOutputs<AppRouter>;
trpc: { type ErrorOutput = TRPCClientError<AppRouter>
abortOnUnmount: true
} const { data: todos, pending, error, refresh } = await useAsyncData<RouterOutput['todo']['getTodos'], ErrorOutput>(() => $client.todo.getTodos.query())
})
</script> </script>
<template> <template>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
const route = useRoute() const route = useRoute()
const { $client } = useNuxtApp() const { $client } = useNuxtApp()
const { data: todo, pending, error } = await $client.todo.getTodo.query(Number(route.params.id)) const { data: todo, pending, error } = await useAsyncData(() => $client.todo.getTodo.query(Number(route.params.id)))
</script> </script>
<template> <template>

View File

@@ -1,10 +1,9 @@
import { httpBatchLink, loggerLink } from '@trpc/client' import { createTRPCProxyClient, httpBatchLink, loggerLink } from '@trpc/client'
import { createTRPCNuxtProxyClient } from 'trpc-nuxt/client'
import superjson from 'superjson' import superjson from 'superjson'
import type { AppRouter } from '~~/server/trpc/routers' import type { AppRouter } from '~~/server/trpc/routers'
export default defineNuxtPlugin(() => { export default defineNuxtPlugin(() => {
const client = createTRPCNuxtProxyClient<AppRouter>({ const client = createTRPCProxyClient<AppRouter>({
transformer: superjson, transformer: superjson,
links: [ links: [
// adds pretty logs to your console in development and logs errors in production // adds pretty logs to your console in development and logs errors in production

2069
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,66 +0,0 @@
import type { CreateTRPCClientOptions, inferRouterProxyClient } from '@trpc/client'
import { createTRPCProxyClient } from '@trpc/client'
import type {
AnyRouter
} from '@trpc/server'
import { createFlatProxy, createRecursiveProxy } from '@trpc/server/shared'
import { hash } from 'ohash'
import { nanoid } from 'nanoid'
import type { DecoratedProcedureRecord } from './types'
// @ts-expect-error: Nuxt auto-imports
import { getCurrentInstance, onScopeDispose, useAsyncData } from '#imports'
/**
* Calculates the key used for `useAsyncData` call
*/
export function getQueryKey (
path: string,
input: unknown
): string {
return input === undefined ? path : `${path}-${hash(input || '')}`
}
function createNuxtProxyDecoration<TRouter extends AnyRouter> (name: string, client: inferRouterProxyClient<TRouter>) {
return createRecursiveProxy((opts) => {
const args = opts.args
const pathCopy = [name, ...opts.path]
// The last arg is for instance `.mutate` or `.query()`
const lastArg = pathCopy.pop()!
const path = pathCopy.join('.')
const [input, otherOptions] = args
const queryKey = lastArg === 'mutate' ? nanoid() : getQueryKey(path, input)
const { trpc, ...asyncDataOptions } = otherOptions || {} as any
let controller: AbortController
if (trpc?.abortOnUnmount) {
if (getCurrentInstance()) {
onScopeDispose(() => {
controller?.abort?.()
})
}
controller = typeof AbortController !== 'undefined' ? new AbortController() : {} as AbortController
}
return useAsyncData(queryKey, () => (client as any)[path][lastArg](input, {
signal: controller?.signal,
...trpc
}), asyncDataOptions)
})
}
export function createTRPCNuxtProxyClient<TRouter extends AnyRouter> (opts: CreateTRPCClientOptions<TRouter>) {
const client = createTRPCProxyClient(opts)
const decoratedClient = createFlatProxy((key) => {
return createNuxtProxyDecoration(key, client)
}) as DecoratedProcedureRecord<TRouter['_def']['record'], TRouter>
return decoratedClient
}

View File

@@ -1,85 +0,0 @@
import type { TRPCClientErrorLike, TRPCRequestOptions } from '@trpc/client'
import type { TRPCSubscriptionObserver } from '@trpc/client/dist/internals/TRPCClient'
import type {
AnyMutationProcedure,
AnyProcedure,
AnyQueryProcedure,
AnyRouter,
AnySubscriptionProcedure,
ProcedureArgs,
ProcedureRouterRecord,
inferProcedureInput,
inferProcedureOutput
} from '@trpc/server'
import type { Unsubscribable, inferObservableValue } from '@trpc/server/observable'
import type {
AsyncData,
AsyncDataOptions,
KeyOfRes,
PickFrom,
_Transform
} from 'nuxt/dist/app/composables/asyncData'
// Modified @trpc/client and @trpc/react-query types
// https://github.com/trpc/trpc/blob/next/packages/client/src/createTRPCClientProxy.ts
// https://github.com/trpc/trpc/blob/next/packages/react-query/src/createTRPCReact.tsx
interface TRPCOptions extends TRPCRequestOptions {
abortOnUnmount?: boolean
}
type SubscriptionResolver<
TProcedure extends AnyProcedure,
TRouter extends AnyRouter,
> = (
...args: [
input: ProcedureArgs<TProcedure['_def']>[0],
opts: ProcedureArgs<TProcedure['_def']>[1] &
Partial<
TRPCSubscriptionObserver<
inferObservableValue<inferProcedureOutput<TProcedure>>,
TRPCClientErrorLike<TRouter>
>
>,
]
) => Unsubscribable
type DecorateProcedure<
TProcedure extends AnyProcedure,
TRouter extends AnyRouter,
> = TProcedure extends AnyQueryProcedure
? {
query: <
TData = inferProcedureOutput<TProcedure>,
Transform extends _Transform<TData> = _Transform<TData, TData>,
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>,
>(
input: inferProcedureInput<TProcedure>,
opts?: AsyncDataOptions<TData, Transform, PickKeys> & { trpc: TRPCOptions },
) => AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, TRPCClientErrorLike<TProcedure>>
} : TProcedure extends AnyMutationProcedure ? {
mutate: <
TData = inferProcedureOutput<TProcedure>,
Transform extends _Transform<TData> = _Transform<TData, TData>,
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>,
>(
input: inferProcedureInput<TProcedure>,
opts?: AsyncDataOptions<TData, Transform, PickKeys> & { trpc: TRPCOptions },
) => AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, TRPCClientErrorLike<TProcedure>>
} : TProcedure extends AnySubscriptionProcedure ? {
subscribe: SubscriptionResolver<TProcedure, TRouter>
} : never
/**
* @internal
*/
export type DecoratedProcedureRecord<
TProcedures extends ProcedureRouterRecord,
TRouter extends AnyRouter,
> = {
[TKey in keyof TProcedures]: TProcedures[TKey] extends AnyRouter
? DecoratedProcedureRecord<TProcedures[TKey]['_def']['record'], TRouter>
: TProcedures[TKey] extends AnyProcedure
? DecorateProcedure<TProcedures[TKey], TRouter>
: never;
}

View File

@@ -1 +1,126 @@
export * from './server' import type { ResponseMeta } from '@trpc/server/http'
import { resolveHTTPResponse } from '@trpc/server/http'
import type {
AnyRouter,
ProcedureType,
inferRouterContext,
inferRouterError
} from '@trpc/server'
import {
TRPCError
} from '@trpc/server'
import { createURL } from 'ufo'
import type { H3Event } from 'h3'
import { createError, defineEventHandler, isMethod, readBody } from 'h3'
import type { TRPCResponse } from '@trpc/server/rpc'
type MaybePromise<T> = T | Promise<T>
export type CreateContextFn<TRouter extends AnyRouter> = (event: H3Event) => MaybePromise<inferRouterContext<TRouter>>
export interface ResponseMetaFnPayload<TRouter extends AnyRouter> {
data: TRPCResponse<unknown, inferRouterError<TRouter>>[]
ctx?: inferRouterContext<TRouter>
paths?: string[]
type: ProcedureType | 'unknown'
errors: TRPCError[]
}
export type ResponseMetaFn<TRouter extends AnyRouter> = (opts: ResponseMetaFnPayload<TRouter>) => ResponseMeta
export interface OnErrorPayload<TRouter extends AnyRouter> {
error: TRPCError
type: ProcedureType | 'unknown'
path: string | undefined
req: H3Event['node']['req']
input: unknown
ctx: undefined | inferRouterContext<TRouter>
}
export type OnErrorFn<TRouter extends AnyRouter> = (opts: OnErrorPayload<TRouter>) => void
export interface ResolveHTTPRequestOptions<TRouter extends AnyRouter> {
router: TRouter
createContext?: CreateContextFn<TRouter>
responseMeta?: ResponseMetaFn<TRouter>
onError?: OnErrorFn<TRouter>
batching?: {
enabled: boolean
}
}
function getPath (event: H3Event): string | null {
if (typeof event.context.params.trpc === 'string') { return event.context.params.trpc }
if (Array.isArray(event.context.params.trpc)) { return event.context.params.trpc.join('/') }
return null
}
export function createNuxtApiHandler<TRouter extends AnyRouter> ({
router,
createContext,
responseMeta,
onError,
batching
}: ResolveHTTPRequestOptions<TRouter>) {
return defineEventHandler(async (event) => {
const {
req,
res
} = event.node
const $url = createURL(req.url!)
const path = getPath(event)
if (path === null) {
const error = router.getErrorShape({
error: new TRPCError({
message:
'Param "trpc" not found - is the file named `[trpc]`.ts or `[...trpc].ts`?',
code: 'INTERNAL_SERVER_ERROR'
}),
type: 'unknown',
ctx: undefined,
path: undefined,
input: undefined
})
throw createError({
statusCode: 500,
statusMessage: JSON.stringify(error)
})
}
const httpResponse = await resolveHTTPResponse({
batching,
router,
req: {
method: req.method!,
headers: req.headers,
body: isMethod(event, 'GET') ? null : await readBody(event),
query: $url.searchParams
},
path,
createContext: async () => await createContext?.(event),
responseMeta,
onError: (o) => {
onError?.({
...o,
req
})
}
})
const { status, headers, body } = httpResponse
res.statusCode = status
headers && Object.keys(headers).forEach((key) => {
res.setHeader(key, headers[key]!)
})
return body
})
}

View File

@@ -1,126 +0,0 @@
import type { ResponseMeta } from '@trpc/server/http'
import { resolveHTTPResponse } from '@trpc/server/http'
import type {
AnyRouter,
ProcedureType,
inferRouterContext,
inferRouterError
} from '@trpc/server'
import {
TRPCError
} from '@trpc/server'
import { createURL } from 'ufo'
import type { H3Event } from 'h3'
import { createError, defineEventHandler, isMethod, readBody } from 'h3'
import type { TRPCResponse } from '@trpc/server/rpc'
type MaybePromise<T> = T | Promise<T>
export type CreateContextFn<TRouter extends AnyRouter> = (event: H3Event) => MaybePromise<inferRouterContext<TRouter>>
export interface ResponseMetaFnPayload<TRouter extends AnyRouter> {
data: TRPCResponse<unknown, inferRouterError<TRouter>>[]
ctx?: inferRouterContext<TRouter>
paths?: string[]
type: ProcedureType | 'unknown'
errors: TRPCError[]
}
export type ResponseMetaFn<TRouter extends AnyRouter> = (opts: ResponseMetaFnPayload<TRouter>) => ResponseMeta
export interface OnErrorPayload<TRouter extends AnyRouter> {
error: TRPCError
type: ProcedureType | 'unknown'
path: string | undefined
req: H3Event['req']
input: unknown
ctx: undefined | inferRouterContext<TRouter>
}
export type OnErrorFn<TRouter extends AnyRouter> = (opts: OnErrorPayload<TRouter>) => void
export interface ResolveHTTPRequestOptions<TRouter extends AnyRouter> {
router: TRouter
createContext?: CreateContextFn<TRouter>
responseMeta?: ResponseMetaFn<TRouter>
onError?: OnErrorFn<TRouter>
batching?: {
enabled: boolean
}
}
function getPath (event: H3Event): string | null {
if (typeof event.context.params.trpc === 'string') { return event.context.params.trpc }
if (Array.isArray(event.context.params.trpc)) { return event.context.params.trpc.join('/') }
return null
}
export function createNuxtApiHandler<TRouter extends AnyRouter> ({
router,
createContext,
responseMeta,
onError,
batching
}: ResolveHTTPRequestOptions<TRouter>) {
return defineEventHandler(async (event) => {
const {
req,
res
} = event
const $url = createURL(req.url!)
const path = getPath(event)
if (path === null) {
const error = router.getErrorShape({
error: new TRPCError({
message:
'Param "trpc" not found - is the file named `[trpc]`.ts or `[...trpc].ts`?',
code: 'INTERNAL_SERVER_ERROR'
}),
type: 'unknown',
ctx: undefined,
path: undefined,
input: undefined
})
throw createError({
statusCode: 500,
statusMessage: JSON.stringify(error)
})
}
const httpResponse = await resolveHTTPResponse({
batching,
router,
req: {
method: req.method!,
headers: req.headers,
body: isMethod(event, 'GET') ? null : await readBody(event),
query: $url.searchParams
},
path,
createContext: async () => await createContext?.(event),
responseMeta,
onError: (o) => {
onError?.({
...o,
req
})
}
})
const { status, headers, body } = httpResponse
res.statusCode = status
headers && Object.keys(headers).forEach((key) => {
res.setHeader(key, headers[key]!)
})
return body
})
}

View File

@@ -1,15 +1,15 @@
import { defineConfig } from 'tsup' import { defineConfig } from 'tsup'
export default defineConfig({ export default defineConfig({
entry: ['src/index.ts', 'src/client/index.ts'], entry: ['src/index.ts'],
format: ['cjs', 'esm'], format: ['cjs', 'esm'],
splitting: false, splitting: false,
clean: true, clean: true,
external: ['#app', '#imports'], external: ['#app', '#imports'],
dts: true, dts: true,
outExtension({ format }) { outExtension ({ format }) {
return { return {
js: format === 'esm' ? '.mjs' : `.${format}`, js: format === 'esm' ? '.mjs' : `.${format}`
} }
}, }
}) })