Compare commits

...

74 Commits

Author SHA1 Message Date
Robert Soriano
977a9e1465 release v0.1.24 2022-06-05 17:05:34 -07:00
Robert Soriano
2620379e02 feat: allow changing of src dir 2022-06-05 17:05:13 -07:00
Robert Soriano
e4f42d5322 Merge pull request #10 from nicolesmileyface/feat/support-src-dir
use nuxt srcDir instead of rootDir
2022-06-05 17:02:41 -07:00
Cole Hollant
ad28a9124e use nuxt srcDir instead of rootDir 2022-06-05 11:55:25 -04:00
Robert Soriano
9c8509f79c release v0.1.23 2022-05-27 08:53:18 -07:00
Robert Soriano
2ce29137ce fix: unimported composable 2022-05-27 08:41:14 -07:00
Robert Soriano
e9c5307e23 release v0.1.22 2022-05-27 01:10:10 -07:00
Robert Soriano
bbdabf544c fix: type inference bug with vueuse 2022-05-27 01:10:06 -07:00
Robert Soriano
aed10ac5b8 release v0.1.21 2022-05-27 00:56:58 -07:00
Robert Soriano
4b2c714658 fix: remove module export 2022-05-27 00:56:53 -07:00
Robert Soriano
43e9fefdbd release v0.1.20 2022-05-26 13:48:57 -07:00
Robert Soriano
dabda23976 export module id 2022-05-26 13:48:53 -07:00
Robert Soriano
af89f32275 release v0.1.19 2022-05-26 07:27:20 -07:00
Robert Soriano
1f27b871fb fix: unref import from vue 2022-05-26 07:21:42 -07:00
Robert Soriano
281e4c05a0 release v0.1.18 2022-05-24 10:01:23 -07:00
Robert Soriano
38ac520b97 fix: server storage and incorrect imports 2022-05-24 10:01:19 -07:00
Robert Soriano
96ebff619e release v0.1.17 2022-05-24 09:55:59 -07:00
Robert Soriano
f668e4a9b4 docs: update readme 2022-05-24 09:55:35 -07:00
Robert Soriano
58b0165557 update readme 2022-05-24 09:55:05 -07:00
Robert Soriano
3b5e35ef68 feat: add composable for custom client headers 2022-05-24 09:46:15 -07:00
Robert Soriano
48152ead8d refactor: remove some comments 2022-05-24 06:43:42 -07:00
Robert Soriano
7f44e049c0 release v0.1.16 2022-05-23 12:41:55 -07:00
Robert Soriano
5a71bbf1fe refactor: use trpc url from config 2022-05-23 12:41:49 -07:00
Robert Soriano
285487e9bf release v0.1.15 2022-05-23 11:24:10 -07:00
Robert Soriano
2cfa64fcc6 fix: unresolvable runtime config 2022-05-23 11:24:07 -07:00
Robert Soriano
eea5733dcd release v0.1.14 2022-05-23 11:09:16 -07:00
Robert Soriano
71bbbf2b86 fix: trpc client composable type 2022-05-23 11:08:52 -07:00
Robert Soriano
2b57ab8791 release v0.1.13 2022-05-23 11:06:58 -07:00
Robert Soriano
f8edd769f0 fix: trpc client composable type 2022-05-23 11:06:51 -07:00
Robert Soriano
419ef34de6 release v0.1.12 2022-05-23 11:03:53 -07:00
Robert Soriano
30c76b5859 fix: client type declaration 2022-05-23 11:03:50 -07:00
Robert Soriano
2575beae5d release v0.1.11 2022-05-23 10:42:19 -07:00
Robert Soriano
b09d1af30d fix: client type declaration 2022-05-23 10:42:16 -07:00
Robert Soriano
610e441db7 release v0.1.10 2022-05-23 10:31:53 -07:00
Robert Soriano
959b370729 fix: unable to resolve nuxt instance 2022-05-23 10:31:48 -07:00
Robert Soriano
c1c4e67694 release v0.1.9 2022-05-23 10:26:02 -07:00
Robert Soriano
779221d9e6 refactor: plugin arrangement 2022-05-23 10:25:34 -07:00
Robert Soriano
986b661e99 release v0.1.8 2022-05-23 10:04:13 -07:00
Robert Soriano
77325a6699 fix: headers missing 2022-05-23 10:04:06 -07:00
Robert Soriano
6dcb4ce8a6 refactor: remove useless transpiled build 2022-05-20 14:26:45 -07:00
Robert Soriano
c95d46f43a release v0.1.7 2022-05-20 11:44:02 -07:00
Robert Soriano
2844cc0bbd refactor: remove helper types 2022-05-20 11:43:58 -07:00
Robert Soriano
aeb2e1b8e3 docs: add inference helpers recipe 2022-05-20 11:42:40 -07:00
Robert Soriano
68d9eb2461 release v0.1.6 2022-05-20 11:30:09 -07:00
Robert Soriano
109a07a42d feat: add helper types 2022-05-20 11:30:04 -07:00
Robert Soriano
7775e59b0c release v0.1.5 2022-05-20 11:26:48 -07:00
Robert Soriano
c23af214a3 remove helpers 2022-05-20 11:26:43 -07:00
Robert Soriano
7851846ad5 release v0.1.4 2022-05-20 11:22:30 -07:00
Robert Soriano
f9b0aa002e refactor: type helpers 2022-05-20 11:22:19 -07:00
Robert Soriano
eb1bd0c700 feat: add trpc type helpers 2022-05-20 11:07:12 -07:00
Robert Soriano
47ba58c0b7 release v0.1.3 2022-05-20 10:19:55 -07:00
Robert Soriano
a458ed9b3f refactor: use full path for router type 2022-05-20 10:08:53 -07:00
Robert Soriano
09d043d49b docs: add recommended ide setup 2022-05-20 10:04:48 -07:00
Robert Soriano
3e47e2a389 Update README.md 2022-05-20 09:14:40 -07:00
Robert Soriano
f8d82c4af1 remove unused module 2022-05-20 08:18:29 -07:00
Robert Soriano
e31578bf97 refactor: remove unused transpile 2022-05-20 00:58:27 -07:00
Robert Soriano
88c77f6e8f release v0.1.2 2022-05-20 00:50:03 -07:00
Robert Soriano
eafc476544 fix types 2022-05-20 00:49:56 -07:00
Robert Soriano
dc86c0252e fix: build error 2022-05-20 00:47:21 -07:00
Robert Soriano
a3dadb9e50 fix demo gif 2022-05-19 14:11:22 -07:00
Robert Soriano
5331535237 update demo gif 2022-05-19 14:07:48 -07:00
Robert Soriano
ae0abd2b45 add demo gif 2022-05-19 14:05:44 -07:00
Robert Soriano
273215c9e1 update readme 2022-05-19 10:56:54 -07:00
Robert Soriano
812ceda4a0 update merging routers example 2022-05-19 10:54:08 -07:00
Robert Soriano
c48556e24e add merging routers recipe 2022-05-19 10:50:50 -07:00
Robert Soriano
f3e0165dd8 update dev playground 2022-05-19 10:26:13 -07:00
Robert Soriano
87ed453425 update playground 2022-05-19 10:05:17 -07:00
Robert Soriano
d890633cb1 remove test line 2022-05-19 09:37:37 -07:00
Robert Soriano
a48ee2551e update local pnpm to 7.1.1 2022-05-19 09:26:22 -07:00
Robert Soriano
899369a0a6 release v0.1.1 2022-05-19 09:25:34 -07:00
Robert Soriano
08857b0f45 remove clean-publish config 2022-05-19 09:25:29 -07:00
Robert Soriano
5b94433b8f use trpc url options in api 2022-05-19 09:25:14 -07:00
Robert Soriano
38fb3edf22 update readme 2022-05-19 01:37:45 -07:00
Robert Soriano
4f2f88bea2 remove clean-publish 2022-05-19 01:23:20 -07:00
36 changed files with 486 additions and 1011 deletions

View File

@@ -4,6 +4,17 @@
End-to-end typesafe APIs with [tRPC.io](https://trpc.io/) in Nuxt applications.
<p align="center">
<figure>
<img src="https://i.imgur.com/AjmNUxj.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>
## Install
```bash
@@ -63,11 +74,9 @@ const client = useClient() // auto-imported
const users = await client.query('getUsers')
const addUser = async () => {
const mutate = await client.mutation('createUser', {
name: 'wagmi'
})
}
const newUser = await client.mutation('createUser', {
name: 'wagmi'
})
```
## useAsyncQuery
@@ -86,13 +95,28 @@ const {
refresh
} = await useAsyncQuery(['getUser', { id: 69 }], {
// pass useAsyncData options here
server: true
lazy: false
})
```
## useClientHeaders
A composable that lets you add additional properties to pass to the tRPC Client. It uses `useStorage` from [@vueuse/core](https://vueuse.org/core/usestorage).
```ts
const headers = useClientHeaders()
const { data: token } = await useAsyncQuery(['auth.login', { username, password }])
headers.value.Authorization = `Bearer ${token}`
// All client calls will now include the Authorization header.
// For SSR, please follow this until I found a solution https://github.com/trpc/trpc/discussions/1686
```
## Options
trpc-nuxt accepts the following options exposed under `~/server/trpc.index.ts`:
trpc-nuxt accepts the following options exposed under `~/server/trpc/index.ts`:
```ts
import * as trpc from '@trpc/server'
@@ -131,11 +155,17 @@ export const onError = (payload: OnErrorPayload<typeof router>) => {
- [Validation](/recipes/validation.md)
- [Authorization](/recipes/authorization.md)
- [Merging Routers](/recipes/merging-routers.md)
- [Error Handling](/recipes/error-handling.md)
- [Error Formatting](/recipes/error-formatting.md)
- [Inference Helpers](/recipes/inference-helpers.md)
Learn more about tRPC.io [here](https://trpc.io/docs).
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
## License
MIT

View File

@@ -1,11 +1,8 @@
{
"name": "trpc-nuxt",
"type": "module",
"version": "0.1.0",
"publishConfig": {
"directory": "package"
},
"packageManager": "pnpm@7.1.0",
"version": "0.1.24",
"packageManager": "pnpm@7.1.1",
"license": "MIT",
"main": "./dist/module.cjs",
"types": "./dist/types.d.ts",
@@ -24,15 +21,11 @@
"dist",
"*.d.ts"
],
"clean-publish": {
"withoutPublish": true,
"tempDir": "package"
},
"scripts": {
"prepublishOnly": "rimraf ./package && nr build && clean-publish",
"postpublish": "rimraf ./package",
"prepublishOnly": "nr build",
"build": "nuxt-module-build",
"play": "nr build && nuxi dev playground",
"build:playground": "nuxi build playground",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"release": "bumpp --commit --push --tag && pnpm publish",
@@ -42,7 +35,10 @@
"@nuxt/kit": "^3.0.0-rc.3",
"@trpc/client": "^9.23.3",
"@trpc/server": "^9.23.2",
"fs-extra": "^10.1.0",
"@vueuse/core": "^8.5.0",
"@vueuse/nuxt": "^8.5.0",
"dedent": "^0.7.0",
"defu": "^6.0.0",
"h3": "^0.7.8",
"pathe": "^0.3.0",
"ufo": "^0.8.4"
@@ -51,13 +47,11 @@
"@antfu/eslint-config": "^0.23.1",
"@antfu/ni": "^0.16.2",
"@nuxt/module-builder": "latest",
"@types/fs-extra": "^9.0.13",
"bumpp": "^7.1.1",
"clean-publish": "^4.0.0",
"eslint": "^8.14.0",
"nuxt": "^3.0.0-rc.3",
"ohash": "^0.1.0",
"pnpm": "^7.1.0",
"pnpm": "^7.1.1",
"superjson": "^1.9.1",
"trpc-nuxt": "workspace:*",
"zod": "^3.16.0"

51
package/.gitignore vendored
View File

@@ -1,51 +0,0 @@
# Dependencies
node_modules
# Logs
*.log*
# Temp directories
.temp
.tmp
.cache
# Yarn
**/.yarn/cache
**/.yarn/*state*
# Generated dirs
dist
# Nuxt
.nuxt
.output
.vercel_build_output
.build-*
.env
.netlify
# Env
.env
# Testing
reports
coverage
*.lcov
.nyc_output
# VSCode
.vscode
# Intellij idea
*.iml
.idea
# OSX
.DS_Store
.AppleDouble
.LSOverride
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

View File

@@ -1,2 +0,0 @@
ignore-workspace-root-check=true
shamefully-hoist=true

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 Robert Soriano <https://github.com/wobsoriano>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,141 +0,0 @@
# tRPC-Nuxt
[![Version](https://img.shields.io/npm/v/trpc-nuxt?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/trpc-nuxt)
End-to-end typesafe APIs with [tRPC.io](https://trpc.io/) in Nuxt applications.
## Install
```bash
npm i trpc-nuxt
```
```ts
// nuxt.config.ts
import { defineNuxtConfig } from 'nuxt'
export default defineNuxtConfig({
modules: ['trpc-nuxt'],
trpc: {
baseURL: 'http://localhost:3000', // defaults to http://localhost:3000
trpcURL: '/api/trpc', // defaults to /api/trpc
},
typescript: {
strict: true // set this to true to make input/output types work
}
})
```
## Usage
Expose your tRPC [routes](https://trpc.io/docs/router) under `~/server/trpc/index.ts`:
```ts
// ~/server/trpc/index.ts
import type { inferAsyncReturnType } from '@trpc/server'
import * as trpc from '@trpc/server'
import { z } from 'zod' // yup/superstruct/zod/myzod/custom
export const router = trpc.router()
// queries and mutations...
.query('getUsers', {
async resolve(req) {
// use your ORM of choice
return await UserModel.all()
},
})
.mutation('createUser', {
// validate input with Zod
input: z.object({ name: z.string().min(5) }),
async resolve(req) {
// use your ORM of choice
return await UserModel.create({
data: req.input,
})
},
})
```
Use the client like so:
```ts
const client = useClient() // auto-imported
const users = await client.query('getUsers')
const addUser = async () => {
const mutate = await client.mutation('createUser', {
name: 'wagmi'
})
}
```
## useAsyncQuery
A thin wrapper around [`useAsyncData`](https://v3.nuxtjs.org/api/composables/use-async-data/) and `client.query()`.
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 {
data,
pending,
error,
refresh
} = await useAsyncQuery(['getUser', { id: 69 }], {
// pass useAsyncData options here
server: true
})
```
## Options
trpc-nuxt accepts the following options exposed under `~/server/trpc.index.ts`:
```ts
import * as trpc from '@trpc/server'
import type { inferAsyncReturnType } from '@trpc/server'
import type { CompatibilityEvent } from 'h3'
import type { OnErrorPayload } from 'trpc-nuxt/api'
export const router = trpc.router<inferAsyncReturnType<typeof createContext>>()
// Optional
// https://trpc.io/docs/context
export const createContext = (event: CompatibilityEvent) => {
// ...
return {
/** context data */
}
}
// Optional
// https://trpc.io/docs/caching#using-responsemeta--to-cache-responses
export const responseMeta = () => {
// ...
return {
// { headers: ... }
}
}
// Optional
// https://trpc.io/docs/error-handling#handling-errors
export const onError = (payload: OnErrorPayload<typeof router>) => {
// Do whatever here like send to bug reporting and stuff
}
```
## Recipes
- [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).
## License
MIT

1
package/api.d.ts vendored
View File

@@ -1 +0,0 @@
export * from './dist/runtime/api'

View File

@@ -1,39 +0,0 @@
{
"name": "trpc-nuxt",
"type": "module",
"version": "0.0.3",
"publishConfig": {
"directory": "package"
},
"packageManager": "pnpm@7.1.0",
"license": "MIT",
"main": "./dist/module.cjs",
"types": "./dist/types.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": "./dist/module.mjs",
"require": "./dist/module.cjs"
},
"./api": {
"import": "./dist/runtime/api.mjs",
"types": "./dist/runtime/api.d.ts"
}
},
"files": [
"dist",
"*.d.ts"
],
"dependencies": {
"@nuxt/kit": "^3.0.0-rc.3",
"@trpc/client": "^9.23.3",
"@trpc/server": "^9.23.2",
"fs-extra": "^10.1.0",
"h3": "^0.7.8",
"pathe": "^0.3.0",
"ufo": "^0.8.4"
},
"scripts": {
"postpublish": "rimraf ./package"
}
}

View File

@@ -1,7 +0,0 @@
node_modules
*.log*
.nuxt
.nitro
.cache
.output
.env

View File

@@ -1,31 +0,0 @@
# dev playground
## Setup
Make sure to install the dependencies:
```bash
pnpm install
```
## Development Server
Start the development server on http://localhost:3000
```bash
pnpm dev
```
## Production
Build the application for production:
```bash
pnpm build
```
Locally preview production build:
```bash
pnpm preview
```

View File

@@ -1,18 +0,0 @@
<script setup lang="ts">
const { data, error } = await useAsyncQuery(['getUser', { username: 'jcena' }], {
lazy: true,
})
const client = useClient()
</script>
<template>
<div>
<div v-if="data">
{{ JSON.stringify(data, null, 2) }}
</div>
<div v-else-if="error">
asdx {{ JSON.stringify(error, null, 2) }}
</div>
</div>
</template>

View File

@@ -1,13 +0,0 @@
import { defineNuxtConfig } from 'nuxt'
import Module from '..'
// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
modules: [Module],
runtimeConfig: {
baseURL: 'http://localhost:3000',
},
typescript: {
strict: true,
},
})

View File

@@ -1,4 +0,0 @@
{
"name": "playground",
"private": true
}

View File

@@ -1,82 +0,0 @@
// ~/server/trpc/index.ts
import { ZodError, z } from 'zod'
import * as trpc from '@trpc/server'
import type { inferAsyncReturnType } from '@trpc/server'
import type { CompatibilityEvent } from 'h3'
import type { ResponseMetaFnPayload } from 'trpc-nuxt/api'
// import superjson from 'superjson'
const fakeUsers = [
{ id: 1, username: 'jcena' },
{ id: 2, username: 'dbatista' },
{ id: 3, username: 'jbiden' },
]
export const router = trpc
.router<inferAsyncReturnType<typeof createContext>>()
.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
},
})
.query('getUser', {
// validate input with Zod
input: z.object({
username: z.string().min(5),
}),
resolve(req) {
return fakeUsers.find(i => i.username === req.input.username) ?? null
},
})
.mutation('createUser', {
input: z.object({ username: z.string().min(5) }),
resolve(req) {
const newUser = {
id: fakeUsers.length + 1,
username: req.input.username,
}
fakeUsers.push(newUser)
return newUser
},
})
export const createContext = (event: CompatibilityEvent) => {
event.res.setHeader('x-ssr', 1)
return {}
}
export const responseMeta = (opts: ResponseMetaFnPayload<any>) => {
// const nuxtApp = useNuxtApp()
// const client = useClient()
// console.log(opts)
// if (nuxtApp.nuxtState) {
// nuxtApp.nuxtState.trpc = client.runtime.transformer.serialize({
// ctx: opts.ctx,
// errors: opts.errors,
// })
// }
// else {
// nuxtApp.nuxtState = {
// trpc: client.runtime.transformer.serialize({
// ctx: opts.ctx,
// errors: opts.errors,
// }),
// }
// }
return {}
}

View File

@@ -1,2 +0,0 @@
packages:
- playground/*

View File

@@ -1,102 +0,0 @@
## 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/index.ts
import type { inferAsyncReturnType } from '@trpc/server'
import type { CompatibilityEvent } from 'h3'
import { decodeAndVerifyJwtToken } from '~/somewhere/in/your/app/utils'
// The app's context - is generated for each incoming request
export async function createContext({ req }: CompatibilityEvent) {
// 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'd 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>
// [..] Define API handler and app router
```
### Option 1: Authorize using resolver
```ts
import { TRPCError } from '@trpc/server'
export const router = trpc
.router<Context>()
// open for anyone
.query('hello', {
input: z.string().nullish(),
resolve: ({ input, ctx }) => {
return `hello ${input ?? ctx.user?.name ?? 'world'}`
},
})
// checked in resolver
.query('secret', {
resolve: ({ ctx }) => {
if (!ctx.user)
throw new TRPCError({ code: 'UNAUTHORIZED' })
return {
secret: 'sauce',
}
},
})
```
### Option 2: Authorize using middleware
```ts
import * as trpc from '@trpc/server'
import { TRPCError } from '@trpc/server'
// Merging routers: https://trpc.io/docs/merging-routers
export const router = trpc
.router<Context>()
// this is accessible for everyone
.query('hello', {
input: z.string().nullish(),
resolve: ({ input, ctx }) => {
return `hello ${input ?? ctx.user?.name ?? 'world'}`
},
})
.merge(
'admin.',
trpc.router<Context>()
// this protects all procedures defined next in this router
.middleware(async ({ ctx, next }) => {
if (!ctx.user?.isAdmin)
throw new TRPCError({ code: 'UNAUTHORIZED' })
return next()
})
.query('secret', {
resolve: ({ ctx }) => {
return {
secret: 'sauce',
}
},
}),
)
```
Learn more about authorization [here](https://trpc.io/docs/authorization).

View File

@@ -1,41 +0,0 @@
## 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<Context>()
.formatError(({ shape, error }) => {
return {
...shape,
data: {
...shape.data,
zodError:
error.code === 'BAD_REQUEST'
&& error.cause instanceof ZodError
? error.cause.flatten()
: null,
}
}
})
```
### Usage in Vue
```html
<script setup lang="ts">
const { error } = await useAsyncQuery(['getUser', { id: 69 }])
</script>
<template>
<pre v-if="error?.data?.zodError">
{{ JSON.stringify(error.data.zodError, null, 2) }}
</pre>
</template>
```
Learn more about error formatting [here](https://trpc.io/docs/error-formatting).

View File

@@ -1,15 +0,0 @@
## Handling errors
All errors that occur in a procedure go through the `onError` method before being sent to the client. Here you can handle or change errors.
```ts
// ~/server/trpc/index.ts
import * as trpc from '@trpc/server'
export function onError({ error, type, path, input, ctx, req }) {
console.error('Error:', error)
if (error.code === 'INTERNAL_SERVER_ERROR') {
// send to bug reporting
}
}
```

View File

@@ -1,49 +0,0 @@
## Validation
tRPC works out-of-the-box with yup/superstruct/zod/myzod/custom validators.
### Input Validation
```ts
// ~/server/trpc/index.ts
import { z } from 'zod'
export const router = trpc
.router()
.mutation('createUser', {
// validate input with Zod
input: z.object({
name: z.string().min(5)
}),
async resolve(req) {
// use your ORM of choice
return await UserModel.create({
data: req.input,
})
},
})
```
### Output Validation
```ts
// ~/server/trpc/index.ts
import { z } from 'zod'
export const router = trpc
.router()
.query('hello', {
// validate output with Zod
output: z.object({
greeting: z.string()
}),
// expects return type of { greeting: string }
resolve() {
return {
greeting: 'hello!',
}
},
})
```
Learn more about input validation [here](https://trpc.io/docs/router#input-validation).

View File

@@ -1,61 +0,0 @@
import { fileURLToPath } from 'url'
import { dirname, join } from 'pathe'
import { addServerHandler, defineNuxtModule } from '@nuxt/kit'
import fs from 'fs-extra'
export interface ModuleOptions {
baseURL: string
trpcURL: string
}
export default defineNuxtModule<ModuleOptions>({
meta: {
name: 'trpc-nuxt',
configKey: 'trpc',
},
defaults: {
baseURL: 'http://localhost:3000',
trpcURL: '/api/trpc',
},
async setup(options, nuxt) {
const runtimeDir = fileURLToPath(new URL('./runtime', import.meta.url))
nuxt.options.build.transpile.push(runtimeDir)
const clientPath = join(nuxt.options.buildDir, 'trpc-client.ts')
const handlerPath = join(nuxt.options.buildDir, 'trpc-handler.ts')
addServerHandler({
route: `${options.trpcURL}/*`,
handler: handlerPath,
})
nuxt.hook('autoImports:extend', (imports) => {
imports.push(
{ name: 'useClient', from: clientPath },
{ name: 'useAsyncQuery', from: join(runtimeDir, 'client') },
)
})
await fs.ensureDir(dirname(clientPath))
await fs.writeFile(clientPath, `
import * as trpc from '@trpc/client'
import type { router } from '~/server/trpc'
const client = trpc.createTRPCClient<typeof router>({
url: '${options.baseURL}${options.trpcURL}',
})
export const useClient = () => client
`)
await fs.writeFile(handlerPath, `
import { createTRPCHandler } from 'trpc-nuxt/api'
import * as functions from '~/server/trpc'
export default createTRPCHandler(functions)
`)
},
})

View File

@@ -1,92 +0,0 @@
import { resolveHTTPResponse } from '@trpc/server'
import type {
AnyRouter,
ProcedureType,
ResponseMeta,
TRPCError,
inferRouterContext,
inferRouterError,
} from '@trpc/server'
import { createURL } from 'ufo'
import type { CompatibilityEvent } from 'h3'
import { defineEventHandler, isMethod, useBody } from 'h3'
import type { TRPCResponse } from '@trpc/server/dist/declarations/src/rpc'
type MaybePromise<T> = T | Promise<T>
export type CreateContextFn<TRouter extends AnyRouter> = (event: CompatibilityEvent) => 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: CompatibilityEvent['req']
input: unknown
ctx: undefined | inferRouterContext<TRouter>
}
export type OnErrorFn<TRouter extends AnyRouter> = (opts: OnErrorPayload<TRouter>) => void
export function createTRPCHandler<Router extends AnyRouter>({
router,
createContext,
responseMeta,
onError,
}: {
router: Router
createContext?: CreateContextFn<Router>
responseMeta?: ResponseMetaFn<Router>
onError?: OnErrorFn<Router>
}) {
const url = '/api/trpc'
return defineEventHandler(async (event) => {
const {
req,
res,
} = event
const $url = createURL(req.url)
event.context.hello = 'world'
const httpResponse = await resolveHTTPResponse({
router,
req: {
method: req.method,
headers: req.headers,
body: isMethod(event, 'GET') ? null : await useBody(event),
query: $url.searchParams,
},
path: $url.pathname.substring(url.length + 1),
createContext: async () => createContext?.(event),
responseMeta,
onError: (o) => {
onError?.({
...o,
req,
})
},
})
const { status, headers, body } = httpResponse
res.statusCode = status
Object.keys(headers).forEach((key) => {
res.setHeader(key, headers[key])
})
return body
})
}

View File

@@ -1,64 +0,0 @@
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<any, any, any, any, any, any>,
> = {
[TPath in keyof TObj]: {
input: inferProcedureInput<TObj[TPath]>
output: inferProcedureOutput<TObj[TPath]>
};
}
type TQueries = AppRouter['_def']['queries']
type TError = TRPCClientErrorLike<AppRouter>
type TQueryValues = inferProcedures<AppRouter['_def']['queries']>
export async function useAsyncQuery<
TPath extends keyof TQueryValues & string,
TOutput extends TQueryValues[TPath]['output'] = TQueryValues[TPath]['output'],
Transform extends _Transform<TOutput> = _Transform<TOutput, TOutput>,
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>,
>(
pathAndInput: [path: TPath, ...args: inferHandlerInput<TQueries[TPath]>],
options: AsyncDataOptions<TOutput, Transform, PickKeys> = {},
): Promise<AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, TError>> {
const client = useClient()
const key = `${pathAndInput[0]}-${objectHash(pathAndInput[1] ? JSON.stringify(pathAndInput[1]) : '')}`
const serverError = useState<TError | null>(`error-${key}`, () => null)
const { error, data, ...rest } = await useAsyncData(
key,
() => client.query(...pathAndInput),
options,
)
// @ts-expect-error: Resolved by Nuxt
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
}

View File

@@ -1,3 +0,0 @@
{
"extends": "./playground/.nuxt/tsconfig.json"
}

View File

@@ -1,18 +1,5 @@
<script setup lang="ts">
const { data, error } = await useAsyncQuery(['getUser', { username: 'jcena' }], {
lazy: true,
})
const client = useClient()
</script>
<template>
<div>
<div v-if="data">
{{ JSON.stringify(data, null, 2) }}
</div>
<div v-else-if="error">
asdx {{ JSON.stringify(error, null, 2) }}
</div>
<NuxtPage />
</div>
</template>

View File

@@ -1,5 +1,5 @@
import { defineNuxtConfig } from 'nuxt'
import Module from '..'
import Module from '../src/module'
// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
const counter = useCookie('counter')
counter.value = counter.value || Math.round(Math.random() * 1000)
</script>
<template>
<div>
<h1> Counter: {{ counter || '-' }}</h1>
<button @click="counter = null">
reset
</button>
<button @click="counter--">
-
</button>
<button @click="counter++">
+
</button>
</div>
</template>

View File

@@ -0,0 +1,64 @@
<script setup lang="ts">
const client = useClient()
const headers = useClientHeaders()
const { data: todos, pending, error, refresh } = await useAsyncQuery(['getTodos'])
const addHeader = () => {
// headers.value.cookie = 'counter=69'
console.log(headers.value)
}
const addTodo = async () => {
const title = Math.random().toString(36).slice(2, 7)
try {
const result = await client.mutation('addTodo', {
id: Date.now(),
userId: 69,
title,
completed: false,
})
console.log('Todo: ', result)
}
catch (e) {
console.log(e)
}
}
</script>
<template>
<div v-if="pending">
Loading...
</div>
<div v-else-if="error?.data?.code">
Error: {{ error.data.code }}
</div>
<div v-else-if="todos">
<ul>
<li v-for="t in todos.slice(0, 10)" :key="t.id">
<NuxtLink :class="{ completed: t.completed }" :to="`/todo/${t.id}`">
Title: {{ t.title }}
</NuxtLink>
</li>
</ul>
<button @click="addTodo">
Add Todo
</button>
<button @click="() => refresh()">
Refresh
</button>
<button @click="addHeader">
Add header
</button>
</div>
</template>
<style>
a {
text-decoration: none;
}
.completed {
text-decoration: line-through;
}
</style>

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
const route = useRoute()
const { data: todo, pending, error } = await useAsyncQuery(['getTodo', Number(route.params.id)])
</script>
<template>
<div v-if="pending">
Loading...
</div>
<div v-else-if="error?.data?.code">
{{ error.data.code }}
</div>
<div v-else>
ID: {{ todo.id }} <br>
Title: {{ todo.title }} <br>
Completed: {{ todo.completed }}
</div>
</template>

View File

@@ -1,82 +1,53 @@
// ~/server/trpc/index.ts
import { ZodError, z } from 'zod'
import * as trpc from '@trpc/server'
import type { inferAsyncReturnType } from '@trpc/server'
import { z } from 'zod'
import type { CompatibilityEvent } from 'h3'
import type { ResponseMetaFnPayload } from 'trpc-nuxt/api'
// import superjson from 'superjson'
import { useCookies } from 'h3'
const fakeUsers = [
{ id: 1, username: 'jcena' },
{ id: 2, username: 'dbatista' },
{ id: 3, username: 'jbiden' },
]
const baseURL = 'https://jsonplaceholder.typicode.com'
export const router = trpc
.router<inferAsyncReturnType<typeof createContext>>()
.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
const TodoShape = z.object({
userId: z.number(),
id: z.number(),
title: z.string(),
completed: z.boolean(),
})
export type Todo = z.infer<typeof TodoShape>
export const router = trpc.router<Context>()
.query('getTodos', {
async resolve() {
return await $fetch<Todo[]>(`${baseURL}/todos`)
},
})
.query('getUser', {
// validate input with Zod
input: z.object({
username: z.string().min(5),
}),
resolve(req) {
return fakeUsers.find(i => i.username === req.input.username) ?? null
.query('getTodo', {
input: z.number(),
async resolve(req) {
return await $fetch<Todo>(`${baseURL}/todos/${req.input}`)
},
})
.mutation('createUser', {
input: z.object({ username: z.string().min(5) }),
resolve(req) {
const newUser = {
id: fakeUsers.length + 1,
username: req.input.username,
}
fakeUsers.push(newUser)
return newUser
.mutation('addTodo', {
input: TodoShape,
async resolve(req) {
return await $fetch<Todo>(`${baseURL}/todos`, {
method: 'POST',
body: req.input,
})
},
})
export const createContext = (event: CompatibilityEvent) => {
event.res.setHeader('x-ssr', 1)
return {}
export async function createContext(event: CompatibilityEvent) {
// 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'd might want to do in your ctx fn
// const x = useCookies(event)
console.log(event.req.headers)
return {
}
}
export const responseMeta = (opts: ResponseMetaFnPayload<any>) => {
// const nuxtApp = useNuxtApp()
// const client = useClient()
// console.log(opts)
// if (nuxtApp.nuxtState) {
// nuxtApp.nuxtState.trpc = client.runtime.transformer.serialize({
// ctx: opts.ctx,
// errors: opts.errors,
// })
// }
// else {
// nuxtApp.nuxtState = {
// trpc: client.runtime.transformer.serialize({
// ctx: opts.ctx,
// errors: opts.errors,
// }),
// }
// }
return {}
}
type Context = inferAsyncReturnType<typeof createContext>

106
pnpm-lock.yaml generated
View File

@@ -10,16 +10,17 @@ importers:
'@nuxt/module-builder': latest
'@trpc/client': ^9.23.3
'@trpc/server': ^9.23.2
'@types/fs-extra': ^9.0.13
'@vueuse/core': ^8.5.0
'@vueuse/nuxt': ^8.5.0
bumpp: ^7.1.1
clean-publish: ^4.0.0
dedent: ^0.7.0
defu: ^6.0.0
eslint: ^8.14.0
fs-extra: ^10.1.0
h3: ^0.7.8
nuxt: ^3.0.0-rc.3
ohash: ^0.1.0
pathe: ^0.3.0
pnpm: ^7.1.0
pnpm: ^7.1.1
superjson: ^1.9.1
trpc-nuxt: workspace:*
ufo: ^0.8.4
@@ -28,7 +29,10 @@ importers:
'@nuxt/kit': 3.0.0-rc.3
'@trpc/client': 9.23.4_@trpc+server@9.23.4
'@trpc/server': 9.23.4
fs-extra: 10.1.0
'@vueuse/core': 8.5.0
'@vueuse/nuxt': 8.5.0
dedent: 0.7.0
defu: 6.0.0
h3: 0.7.8
pathe: 0.3.0
ufo: 0.8.4
@@ -36,9 +40,7 @@ importers:
'@antfu/eslint-config': 0.23.1_eslint@8.15.0
'@antfu/ni': 0.16.2
'@nuxt/module-builder': 0.1.7
'@types/fs-extra': 9.0.13
bumpp: 7.1.1
clean-publish: 4.0.0
eslint: 8.15.0
nuxt: 3.0.0-rc.3
ohash: 0.1.0
@@ -945,12 +947,6 @@ packages:
resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==}
dev: true
/@types/fs-extra/9.0.13:
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
dependencies:
'@types/node': 17.0.34
dev: true
/@types/jsdom/16.2.14:
resolution: {integrity: sha512-6BAy1xXEmMuHeAJ4Fv4yXKwBDTGTOseExKE3OaHiNycdHdZw59KfYzrt0DkDluvwmik1HRt6QS7bImxUmpSy+w==}
dependencies:
@@ -1274,6 +1270,22 @@ packages:
resolution: {integrity: sha512-UBc1Pg1T3yZ97vsA2ueER0F6GbJebLHYlEi4ou1H5YL4KWvMOOWwpYo9/QpWq93wxKG6Wo13IY74Hcn/f7c7Bg==}
dev: true
/@vueuse/core/8.5.0:
resolution: {integrity: sha512-VEJ6sGNsPlUp0o9BGda2YISvDZbhWJSOJu5zlp2TufRGVrLcYUKr31jyFEOj6RXzG3k/H4aCYeZyjpItfU8glw==}
peerDependencies:
'@vue/composition-api': ^1.1.0
vue: ^2.6.0 || ^3.2.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
vue:
optional: true
dependencies:
'@vueuse/metadata': 8.5.0
'@vueuse/shared': 8.5.0
vue-demi: 0.12.5
dev: false
/@vueuse/head/0.7.6_vue@3.2.33:
resolution: {integrity: sha512-cOWqCkT3WiF5oEpw+VVEWUJd9RLD5rc7DmnFp3cePsejp+t7686uKD9Z9ZU7Twb7R/BI8iexKTmXo9D/F3v6UA==}
peerDependencies:
@@ -1282,6 +1294,42 @@ packages:
vue: 3.2.33
dev: true
/@vueuse/metadata/8.5.0:
resolution: {integrity: sha512-WxsD+Cd+bn+HcjpY6Dl9FJ8ywTRTT9pTwk3bCQpzEhXVYAyNczKDSahk50fCfIJKeWHhyI4B2+/ZEOxQAkUr0g==}
dev: false
/@vueuse/nuxt/8.5.0:
resolution: {integrity: sha512-riGrDwlTQbjSDxyw46oc1catXpZwzzRrEk+PTy8NQZG6uevgJ8wNuAhPVx/5oG0jYG/t2orIFfc9xGEA6uytSg==}
dependencies:
'@nuxt/kit': 3.0.0-rc.3
'@vueuse/core': 8.5.0
'@vueuse/metadata': 8.5.0
local-pkg: 0.4.1
vue-demi: 0.12.5
transitivePeerDependencies:
- '@vue/composition-api'
- esbuild
- rollup
- supports-color
- vite
- vue
- webpack
dev: false
/@vueuse/shared/8.5.0:
resolution: {integrity: sha512-qKG+SZb44VvGD4dU5cQ63z4JE2Yk39hQUecR0a9sEdJA01cx+XrxAvFKJfPooxwoiqalAVw/ktWK6xbyc/jS3g==}
peerDependencies:
'@vue/composition-api': ^1.1.0
vue: ^2.6.0 || ^3.2.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
vue:
optional: true
dependencies:
vue-demi: 0.12.5
dev: false
/abbrev/1.1.1:
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
dev: true
@@ -1695,16 +1743,6 @@ packages:
resolution: {integrity: sha512-SXgeMX9VwDe7iFFaEWkA5AstuER9YKqy4EhHqr4DVqkwmD9rpVimkMKWHdjn30Ja45txyjhSn63lVX69eVCckg==}
dev: true
/clean-publish/4.0.0:
resolution: {integrity: sha512-PcOxJSnPgncx/ANmgPw8hj9DDtdLHTxIc6Vh/yXCX7IA9w1oUSx2POL0Vno/7omhyrKxMKTpnRh2wlzwH5zaCw==}
hasBin: true
dependencies:
cross-spawn: 7.0.3
fast-glob: 3.2.11
lilconfig: 2.0.5
micromatch: 4.0.5
dev: true
/clean-regexp/1.0.0:
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
engines: {node: '>=4'}
@@ -2041,6 +2079,10 @@ packages:
engines: {node: '>=0.10'}
dev: true
/dedent/0.7.0:
resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==}
dev: false
/deep-extend/0.6.0:
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
engines: {node: '>=4.0.0'}
@@ -3226,6 +3268,7 @@ packages:
graceful-fs: 4.2.10
jsonfile: 6.1.0
universalify: 2.0.0
dev: true
/fs-memo/1.2.0:
resolution: {integrity: sha512-YEexkCpL4j03jn5SxaMHqcO6IuWuqm8JFUYhyCep7Ao89JIYmB8xoKhK7zXXJ9cCaNXpyNH5L3QtAmoxjoHW2w==}
@@ -3757,7 +3800,7 @@ packages:
dev: true
/is-module/1.0.0:
resolution: {integrity: sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=}
resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==}
dev: true
/is-negative-zero/2.0.2:
@@ -3940,6 +3983,7 @@ packages:
universalify: 2.0.0
optionalDependencies:
graceful-fs: 4.2.10
dev: true
/jsx-ast-utils/3.3.0:
resolution: {integrity: sha512-XzO9luP6L0xkxwhIJMTJQpZo/eeN60K08jHdexfD569AGxeNug6UketeHXEhROoM8aR7EcUoOQmIhcJQjcuq8Q==}
@@ -6343,6 +6387,7 @@ packages:
/universalify/2.0.0:
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
engines: {node: '>= 10.0.0'}
dev: true
/unplugin/0.6.3:
resolution: {integrity: sha512-CoW88FQfCW/yabVc4bLrjikN9HC8dEvMU4O7B6K2jsYMPK0l6iAnd9dpJwqGcmXJKRCU9vwSsy653qg+RK0G6A==}
@@ -6578,6 +6623,19 @@ packages:
bundle-runner: 0.0.1
dev: true
/vue-demi/0.12.5:
resolution: {integrity: sha512-BREuTgTYlUr0zw0EZn3hnhC3I6gPWv+Kwh4MCih6QcAeaTlaIX0DwOVN0wHej7hSvDPecz4jygy/idsgKfW58Q==}
engines: {node: '>=12'}
hasBin: true
requiresBuild: true
peerDependencies:
'@vue/composition-api': ^1.0.0-rc.1
vue: ^3.0.0-0 || ^2.6.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
dev: false
/vue-eslint-parser/8.3.0_eslint@8.15.0:
resolution: {integrity: sha512-dzHGG3+sYwSf6zFBa0Gi9ZDshD7+ad14DGOdTLjruRVgZXe2J+DcZ9iUhyR48z5g1PqRa20yt3Njna/veLJL/g==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}

View File

@@ -0,0 +1,80 @@
## Inference Helpers
`@trpc/server` exports the following helper types to assist with inferring these types from the `router` exported in `~/server/trpc/index.ts`:
- `inferProcedureOutput<TProcedure>`
- `inferProcedureInput<TProcedure>`
- `inferSubscriptionOutput<TRouter, TPath>`
```ts
// ~/utils/trpc.ts
import type { router } from '~/server/trpc/index.ts'
type AppRouter = typeof router
/**
* Enum containing all api query paths
*/
export type TQuery = keyof AppRouter['_def']['queries']
/**
* Enum containing all api mutation paths
*/
export type TMutation = keyof AppRouter['_def']['mutations']
/**
* Enum containing all api subscription paths
*/
export type TSubscription = keyof AppRouter['_def']['subscriptions']
/**
* This is a helper method to infer the output of a query resolver
* @example type HelloOutput = InferQueryOutput<'hello'>
*/
export type InferQueryOutput<TRouteKey extends TQuery> = inferProcedureOutput<
AppRouter['_def']['queries'][TRouteKey]
>
/**
* This is a helper method to infer the input of a query resolver
* @example type HelloInput = InferQueryInput<'hello'>
*/
export type InferQueryInput<TRouteKey extends TQuery> = inferProcedureInput<
AppRouter['_def']['queries'][TRouteKey]
>
/**
* This is a helper method to infer the output of a mutation resolver
* @example type HelloOutput = InferMutationOutput<'hello'>
*/
export type InferMutationOutput<TRouteKey extends TMutation> =
inferProcedureOutput<AppRouter['_def']['mutations'][TRouteKey]>
/**
* This is a helper method to infer the input of a mutation resolver
* @example type HelloInput = InferMutationInput<'hello'>
*/
export type InferMutationInput<TRouteKey extends TMutation> =
inferProcedureInput<AppRouter['_def']['mutations'][TRouteKey]>
/**
* This is a helper method to infer the output of a subscription resolver
* @example type HelloOutput = InferSubscriptionOutput<'hello'>
*/
export type InferSubscriptionOutput<TRouteKey extends TSubscription> =
inferProcedureOutput<AppRouter['_def']['subscriptions'][TRouteKey]>
/**
* This is a helper method to infer the asynchronous output of a subscription resolver
* @example type HelloAsyncOutput = InferAsyncSubscriptionOutput<'hello'>
*/
export type InferAsyncSubscriptionOutput<TRouteKey extends TSubscription> =
inferSubscriptionOutput<AppRouter, TRouteKey>
/**
* This is a helper method to infer the input of a subscription resolver
* @example type HelloInput = InferSubscriptionInput<'hello'>
*/
export type InferSubscriptionInput<TRouteKey extends TSubscription> =
inferProcedureInput<AppRouter['_def']['subscriptions'][TRouteKey]>
```

View File

@@ -0,0 +1,46 @@
# Merging Routers
Writing all API-code in your code in the same file is not a great idea. It's easy to merge routers with other routers.
Define your routes:
```ts
// ~/server/trpc/routes/posts.ts
export const posts = trpc.router()
.query('list', {
resolve() {
// ..
return []
}
})
```
```ts
// ~/server/trpc/routes/users.ts
export const users = trpc.router()
.query('list', {
resolve() {
// ..
return []
}
})
```
```ts
// ~/server/trpc/index.ts
import { users } from './routes/users'
import { posts } from './routes/posts'
export const router = trpc.router()
.merge('user.', users) // prefix user procedures with "user."
.merge('post.', posts) // prefix post procedures with "post."
```
and use it like this:
```html
<script setup lang="ts">
const { data: users } = await useAsyncQuery(['user.list'])
const { data: posts } = await useAsyncQuery(['post.list'])
</script>
```

View File

@@ -1,8 +1,10 @@
import { fileURLToPath } from 'url'
import { dirname, join } from 'pathe'
import { join, resolve } from 'pathe'
import { defu } from 'defu'
// @ts-expect-error: No types
import dedent from 'dedent'
import { addServerHandler, defineNuxtModule } from '@nuxt/kit'
import fs from 'fs-extra'
import { addPlugin, addServerHandler, addTemplate, defineNuxtModule } from '@nuxt/kit'
export interface ModuleOptions {
baseURL: string
@@ -20,42 +22,50 @@ export default defineNuxtModule<ModuleOptions>({
},
async setup(options, nuxt) {
const runtimeDir = fileURLToPath(new URL('./runtime', import.meta.url))
nuxt.options.build.transpile.push(runtimeDir)
nuxt.options.build.transpile.push(runtimeDir, '#build/trpc-handler')
const clientPath = join(nuxt.options.buildDir, 'trpc-client.ts')
const handlerPath = join(nuxt.options.buildDir, 'trpc-handler.ts')
const trpcOptionsPath = join(nuxt.options.srcDir, 'server/trpc')
addServerHandler({
route: `${options.trpcURL}/*`,
handler: handlerPath,
// Add vueuse
nuxt.options.modules.push('@vueuse/nuxt')
// Final resolved configuration
const finalConfig = nuxt.options.runtimeConfig.public.trpc = defu(nuxt.options.runtimeConfig.public.trpc, {
baseURL: options.baseURL,
trpcURL: options.trpcURL,
})
nuxt.hook('autoImports:extend', (imports) => {
imports.push(
{ name: 'useClient', from: clientPath },
{ name: 'useClient', from: join(runtimeDir, 'client') },
{ name: 'useAsyncQuery', from: join(runtimeDir, 'client') },
{ name: 'useClientHeaders', from: join(runtimeDir, 'client') },
)
})
await fs.ensureDir(dirname(clientPath))
addServerHandler({
route: `${finalConfig.trpcURL}/*`,
handler: handlerPath,
})
await fs.writeFile(clientPath, `
import * as trpc from '@trpc/client'
import type { router } from '~/server/trpc'
addPlugin(resolve(runtimeDir, 'plugin'))
const client = trpc.createTRPCClient<typeof router>({
url: '${options.baseURL}${options.trpcURL}',
})
addTemplate({
filename: 'trpc-handler.ts',
write: true,
getContents() {
return dedent`
import { createTRPCHandler } from 'trpc-nuxt/api'
import * as functions from '${trpcOptionsPath}'
export const useClient = () => client
`)
await fs.writeFile(handlerPath, `
import { createTRPCHandler } from 'trpc-nuxt/api'
import * as functions from '~/server/trpc'
export default createTRPCHandler(functions)
`)
export default createTRPCHandler({
...functions,
trpcURL: '${finalConfig.trpcURL}'
})
`
},
})
},
})

View File

@@ -42,14 +42,14 @@ export function createTRPCHandler<Router extends AnyRouter>({
createContext,
responseMeta,
onError,
trpcURL,
}: {
router: Router
createContext?: CreateContextFn<Router>
responseMeta?: ResponseMetaFn<Router>
onError?: OnErrorFn<Router>
trpcURL: string
}) {
const url = '/api/trpc'
return defineEventHandler(async (event) => {
const {
req,
@@ -58,8 +58,6 @@ export function createTRPCHandler<Router extends AnyRouter>({
const $url = createURL(req.url)
event.context.hello = 'world'
const httpResponse = await resolveHTTPResponse({
router,
req: {
@@ -68,7 +66,7 @@ export function createTRPCHandler<Router extends AnyRouter>({
body: isMethod(event, 'GET') ? null : await useBody(event),
query: $url.searchParams,
},
path: $url.pathname.substring(url.length + 1),
path: $url.pathname.substring(trpcURL.length + 1),
createContext: async () => createContext?.(event),
responseMeta,
onError: (o) => {

View File

@@ -6,18 +6,18 @@ import type {
_Transform,
} from 'nuxt/dist/app/composables/asyncData'
import type { ProcedureRecord, inferHandlerInput, inferProcedureInput, inferProcedureOutput } from '@trpc/server'
import type { TRPCClientErrorLike } from '@trpc/client'
import type { TRPCClient, 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 { Ref } from 'vue'
import { useStorage } from '@vueuse/core'
import { useAsyncData, useNuxtApp, useState } from '#app'
import type { router } from '~/server/trpc'
type MaybeRef<T> = T | Ref<T>
type AppRouter = typeof router
type inferProcedures<
export type inferProcedures<
TObj extends ProcedureRecord<any, any, any, any, any, any>,
> = {
[TPath in keyof TObj]: {
@@ -26,10 +26,10 @@ type inferProcedures<
};
}
type TQueries = AppRouter['_def']['queries']
type TError = TRPCClientErrorLike<AppRouter>
export type TQueries = AppRouter['_def']['queries']
export type TError = TRPCClientErrorLike<AppRouter>
type TQueryValues = inferProcedures<AppRouter['_def']['queries']>
export type TQueryValues = inferProcedures<AppRouter['_def']['queries']>
export async function useAsyncQuery<
TPath extends keyof TQueryValues & string,
@@ -40,16 +40,16 @@ export async function useAsyncQuery<
pathAndInput: [path: TPath, ...args: inferHandlerInput<TQueries[TPath]>],
options: AsyncDataOptions<TOutput, Transform, PickKeys> = {},
): Promise<AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, TError>> {
const client = useClient()
const { $client } = useNuxtApp()
const key = `${pathAndInput[0]}-${objectHash(pathAndInput[1] ? JSON.stringify(pathAndInput[1]) : '')}`
const serverError = useState<TError | null>(`error-${key}`, () => null)
const { error, data, ...rest } = await useAsyncData(
key,
() => client.query(...pathAndInput),
() => $client.query(...pathAndInput),
// @ts-expect-error: Internal
options,
)
// @ts-expect-error: Resolved by Nuxt
if (process.server && error.value && !serverError.value)
serverError.value = error.value as any
@@ -62,3 +62,12 @@ export async function useAsyncQuery<
error: serverError,
} as any
}
export function useClient(): TRPCClient<AppRouter> {
const { $client } = useNuxtApp()
return $client
}
export function useClientHeaders(initialValue: MaybeRef<Record<string, any>> = {}): Ref<Record<string, any>> {
return useStorage('trpc-nuxt-header', initialValue)
}

30
src/runtime/plugin.ts Normal file
View File

@@ -0,0 +1,30 @@
import * as trpc from '@trpc/client'
import { unref } from 'vue'
import { useClientHeaders } from './client'
import { defineNuxtPlugin, useRequestHeaders, useRuntimeConfig } from '#app'
import type { router } from '~/server/trpc'
declare type AppRouter = typeof router
export default defineNuxtPlugin((nuxtApp) => {
const config = useRuntimeConfig().public.trpc
const headers = useRequestHeaders()
const otherHeaders = useClientHeaders()
const client = trpc.createTRPCClient<AppRouter>({
url: `${config.baseURL}${config.trpcURL}`,
headers: () => {
return {
...unref(otherHeaders),
...headers,
}
},
})
nuxtApp.provide('client', client)
})
declare module '#app' {
interface NuxtApp {
$client: trpc.TRPCClient<AppRouter>
}
}