From 2526e1e6854709b1c9630ff2aac7bf218fec7d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjam=C3=ADn=20Eidelman?= Date: Fri, 26 Apr 2024 23:46:09 -0300 Subject: [PATCH] feat(trpc): access hono context from trpc procedures (#458) * trpc: access hono context from trpc procedures * chore: Add changeset --- .changeset/dull-carrots-wash.md | 5 ++ packages/trpc-server/README.md | 46 ++++++++++++++++ packages/trpc-server/src/index.ts | 16 ++++-- packages/trpc-server/test/context.test.ts | 67 +++++++++++++++++++++++ 4 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 .changeset/dull-carrots-wash.md create mode 100644 packages/trpc-server/test/context.test.ts diff --git a/.changeset/dull-carrots-wash.md b/.changeset/dull-carrots-wash.md new file mode 100644 index 00000000..ca05636b --- /dev/null +++ b/.changeset/dull-carrots-wash.md @@ -0,0 +1,5 @@ +--- +'@hono/trpc-server': minor +--- + +access hono context from trpc procedures diff --git a/packages/trpc-server/README.md b/packages/trpc-server/README.md index 24750ef3..b04cb52b 100644 --- a/packages/trpc-server/README.md +++ b/packages/trpc-server/README.md @@ -67,6 +67,52 @@ const client = createTRPCProxyClient({ console.log(await client.hello.query('Hono')) ``` +## Context + +You can also access `c.env` from hono context from the trpc `ctx`. eg. here's an example using cloudflare D1 binding available as `env.DB` + +```ts +import { initTRPC } from '@trpc/server' +import { z } from 'zod' + +type Env = { + DB: D1Database; +} +type HonoContext = { + env: Env, +}; + +const t = initTRPC.context().create() + +const publicProcedure = t.procedure +const router = t.router + +export const appRouter = router({ + usersCount: publicProcedure.query(({ input, ctx }) => { + const result = await ctx.env.DB.prepare("SELECT count(*) from user;").all(); + return result.results[0].count; + }), +}) + +export type AppRouter = typeof appRouter +``` + +For further control, you can optionally specify a `createContext` that in this case will receive the hono context as 2nd argument: + +```ts +app.use( + '/trpc/*', + trpcServer({ + router: appRouter, + createContext: (_opts, c) => ({ + // c is the hono context + var1: c.env.MY_VAR1, + var2: c.req.header('X-VAR2'), + }) + }) +) +``` + ## Author Yusuke Wada diff --git a/packages/trpc-server/src/index.ts b/packages/trpc-server/src/index.ts index cc6802b9..c2edd943 100644 --- a/packages/trpc-server/src/index.ts +++ b/packages/trpc-server/src/index.ts @@ -1,15 +1,21 @@ import type { AnyRouter } from '@trpc/server' -import type { FetchHandlerRequestOptions } from '@trpc/server/adapters/fetch' +import type { FetchCreateContextFnOptions, FetchHandlerRequestOptions } from '@trpc/server/adapters/fetch' import { fetchRequestHandler } from '@trpc/server/adapters/fetch' -import type { MiddlewareHandler } from 'hono' +import type { Context, MiddlewareHandler } from 'hono' -type tRPCOptions = Omit, 'req' | 'endpoint'> & - Partial, 'endpoint'>> +type tRPCOptions = Omit, 'req' | 'endpoint' | 'createContext'> & + Partial, 'endpoint'>> & + { createContext? (opts: FetchCreateContextFnOptions, c: Context): Record } -export const trpcServer = ({ endpoint = '/trpc', ...rest }: tRPCOptions): MiddlewareHandler => { +export const trpcServer = ({ endpoint = '/trpc', createContext, ...rest }: tRPCOptions): MiddlewareHandler => { return async (c) => { const res = fetchRequestHandler({ ...rest, + createContext: (opts) => ({ + ...createContext ? createContext(opts, c) : {}, + // propagate env by default + env: c.env, + }), endpoint, req: c.req.raw, }) diff --git a/packages/trpc-server/test/context.test.ts b/packages/trpc-server/test/context.test.ts new file mode 100644 index 00000000..aea93059 --- /dev/null +++ b/packages/trpc-server/test/context.test.ts @@ -0,0 +1,67 @@ +import { initTRPC } from '@trpc/server' +import { Hono } from 'hono' +import { trpcServer } from '../src' + +describe('tRPC Adapter Middleware passing Context', () => { + type Env = { + NAME: string; + } + type HonoContext = { + env: Env, + batch: string; + }; + + const t = initTRPC.context().create() + + const publicProcedure = t.procedure.use( + t.middleware((opts) => { + return opts.next({ + ctx: { + // add .env into context, simulating a middleware as cloudflare pages + env: { + DB: { + getName: () => 'World' + }, + } + }, + }) + }), + ) + const router = t.router + + const appRouter = router({ + hello: publicProcedure.query(({ ctx }) => { + return `Hello ${ctx.env.DB.getName()}, batch is ${ctx.batch}` + }), + }) + + const app = new Hono() + + app.use( + '/trpc/*', + trpcServer({ + router: appRouter, + // optional createContext, additional `c` arg with the hono context + createContext: (_opts, c) => ({ + batch: c.req.query('batch'), + }) + }) + ) + + it.only('Should return 200 response', async () => { + const searchParams = new URLSearchParams({ + input: JSON.stringify({ '0': 'Hono' }), + batch: '1', + }) + const req = new Request(`http://localhost/trpc/hello?${searchParams.toString()}`) + const res = await app.request(req) + expect(res.status).toBe(200) + expect(await res.json()).toEqual([ + { + result: { + data: 'Hello World, batch is 1', + }, + }, + ]) + }) +})