diff --git a/apps/content/docs/openapi/integrations/trpc.md b/apps/content/docs/openapi/integrations/trpc.md index 40eb0b9ac..eea63d06e 100644 --- a/apps/content/docs/openapi/integrations/trpc.md +++ b/apps/content/docs/openapi/integrations/trpc.md @@ -50,6 +50,16 @@ const orpcRouter = toORPCRouter(trpcRouter) ::: warning Ensure you set the `.meta` type to `ORPCMeta` when creating your tRPC builder. This is required for OpenAPI features to function properly. + +```ts +const example = t.procedure + .meta({ route: { path: '/hello', summary: 'Hello procedure' } }) // [!code highlight] + .input(z.object({ name: z.string() })) + .query(({ input }) => { + return `Hello, ${input.name}!` + }) +``` + ::: ### Specification Generation diff --git a/packages/shared/src/object.ts b/packages/shared/src/object.ts index 84ff7a4d2..b98bb9938 100644 --- a/packages/shared/src/object.ts +++ b/packages/shared/src/object.ts @@ -63,7 +63,7 @@ export function clone(value: T): T { return value } -export function get(object: object, path: readonly string[]): unknown { +export function get(object: unknown, path: readonly string[]): unknown { let current: unknown = object for (const key of path) { diff --git a/packages/trpc/src/to-orpc-router.test.ts b/packages/trpc/src/to-orpc-router.test.ts index ea5afc8a6..6ee848d6b 100644 --- a/packages/trpc/src/to-orpc-router.test.ts +++ b/packages/trpc/src/to-orpc-router.test.ts @@ -1,6 +1,6 @@ -import { call, createRouterClient, getEventMeta, isProcedure, ORPCError, unlazy } from '@orpc/server' +import { call, createRouterClient, getEventMeta, isLazy, isProcedure, ORPCError, unlazy } from '@orpc/server' import { isAsyncIteratorObject } from '@orpc/shared' -import { tracked } from '@trpc/server' +import { tracked, TRPCError } from '@trpc/server' import * as z from 'zod' import { inputSchema, outputSchema } from '../../contract/tests/shared' import { t, trpcRouter } from '../tests/shared' @@ -19,11 +19,16 @@ describe('toORPCRouter', async () => { expect(orpcRouter.nested.ping).toSatisfy(isProcedure) const unlazy1 = await unlazy(orpcRouter.lazy) - expect(unlazy1.default.subscribe).toSatisfy(isProcedure) - const unlazy2 = await unlazy(unlazy1.default.lazy) + expect(orpcRouter.lazy).toSatisfy(isLazy) + expect(unlazy1.default.subscribe).toSatisfy(isProcedure) + expect(unlazy1.default.lazy).toSatisfy(isLazy) expect(unlazy2.default.throw).toSatisfy(isProcedure) + + // accessible lazy router + expect(await unlazy(orpcRouter.lazy.subscribe)).toEqual({ default: expect.toSatisfy(isProcedure) }) + expect(await unlazy(orpcRouter.lazy.lazy.throw)).toEqual({ default: expect.toSatisfy(isProcedure) }) }) it('with disabled input/output', async () => { @@ -40,7 +45,7 @@ describe('toORPCRouter', async () => { it('meta/route', async () => { expect(orpcRouter.ping['~orpc'].meta).toEqual({ meta1: 'test' }) expect(orpcRouter.nested.ping['~orpc'].route).toEqual({ path: '/nested/ping', description: 'Nested ping procedure' }) - expect(orpcRouter.nested.ping['~orpc'].meta).toEqual({ path: '/nested/ping', description: 'Nested ping procedure' }) + expect(orpcRouter.nested.ping['~orpc'].meta).toEqual({ route: { path: '/nested/ping', description: 'Nested ping procedure' } }) }) describe('calls', () => { @@ -61,6 +66,16 @@ describe('toORPCRouter', async () => { ).rejects.toSatisfy((err: any) => { return err instanceof ORPCError && err.code === 'PARSE_ERROR' && err.message === 'throw' }) + + await expect( + call(orpcRouter.ping, { input: 'invalid' } as any, { context: { a: 'test' } }), + ).rejects.toSatisfy((err: any) => { + expect(err).toBeInstanceOf(ORPCError) + expect(err.cause).toBeInstanceOf(TRPCError) + expect(err.cause.cause).toBeInstanceOf(z.ZodError) + + return true + }) }) it('deep lazy', async () => { diff --git a/packages/trpc/src/to-orpc-router.ts b/packages/trpc/src/to-orpc-router.ts index f93323493..62ac89067 100644 --- a/packages/trpc/src/to-orpc-router.ts +++ b/packages/trpc/src/to-orpc-router.ts @@ -3,12 +3,12 @@ import type { AnyProcedure, AnyRouter, inferRouterContext } from '@trpc/server' import type { inferRouterMeta, Parser, TrackedData } from '@trpc/server/unstable-core-do-not-import' import { mapEventIterator } from '@orpc/client' import * as ORPC from '@orpc/server' -import { isObject, isTypescriptObject } from '@orpc/shared' +import { get, isObject, isTypescriptObject } from '@orpc/shared' import { isTrackedEnvelope, TRPCError } from '@trpc/server' import { getHTTPStatusCodeFromError, isAsyncIterable } from '@trpc/server/unstable-core-do-not-import' -export interface experimental_ORPCMeta extends ORPC.Route { - +export interface experimental_ORPCMeta { + route?: ORPC.Route } export type experimental_ToORPCOutput @@ -59,10 +59,10 @@ function lazyToORPCRouter(lazies: AnyRouter['_def']['lazy']) { for (const key in lazies) { const item = lazies[key]! - orpcRouter[key] = ORPC.lazy(async () => { + orpcRouter[key] = ORPC.createAccessibleLazyRouter(ORPC.lazy(async () => { const router = await item.ref() return { default: experimental_toORPCRouter(router) } - }) + })) } return orpcRouter @@ -91,7 +91,7 @@ function toORPCProcedure(procedure: AnyProcedure) { meta: procedure._def.meta ?? {}, inputValidationIndex: 0, outputValidationIndex: 0, - route: procedure._def.meta ?? {}, + route: get(procedure._def.meta, ['route']) ?? {}, middlewares: [], inputSchema: toDisabledStandardSchema(procedure._def.inputs.at(-1)), outputSchema: toDisabledStandardSchema((procedure as any)._def.output), diff --git a/packages/trpc/tests/shared.ts b/packages/trpc/tests/shared.ts index 4025de247..e28664982 100644 --- a/packages/trpc/tests/shared.ts +++ b/packages/trpc/tests/shared.ts @@ -40,7 +40,7 @@ export const trpcRouter = t.router({ nested: { ping: t.procedure - .meta({ path: '/nested/ping', description: 'Nested ping procedure' }) + .meta({ route: { path: '/nested/ping', description: 'Nested ping procedure' } }) .input(z.object({ a: z.string() })) .output(z.string().transform(val => Number(val))) .query(({ input }) => {