diff --git a/packages/server/package.json b/packages/server/package.json index 43761fe4..0b8fdfef 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -56,7 +56,6 @@ "@orpc/transformer": "workspace:*" }, "devDependencies": { - "@orpc/openapi": "workspace:*", "zod": "^3.24.1" } } diff --git a/packages/server/src/fetch/handler.test.ts b/packages/server/src/fetch/handler.test.ts deleted file mode 100644 index 005bb20b..00000000 --- a/packages/server/src/fetch/handler.test.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { ORPC_PROTOCOL_HEADER, ORPC_PROTOCOL_VALUE } from '@orpc/shared' -import { z } from 'zod' -import { os } from '..' -import { createORPCHandler } from './handler' - -describe('oRPCHandler', () => { - const handler = createORPCHandler() - - const ping = os.input(z.object({ value: z.string() })).output(z.string()).func((input) => { - return input.value - }) - const pong = os.func(() => 'pong') - - const lazyRouter = os.lazy(() => Promise.resolve({ - default: { - ping: os.lazy(() => Promise.resolve({ default: ping })), - pong, - lazyRouter: os.lazy(() => Promise.resolve({ default: { ping, pong } })), - }, - })) - - const router = os.router({ - ping, - pong, - lazyRouter, - }) - - it('should handle request', async () => { - const response = await handler({ - router, - request: new Request('http://localhost/ping', { - method: 'POST', - headers: { - [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ data: { value: '123' }, meta: [] }), - }), - }) - - expect(response?.status).toEqual(200) - expect(await response?.json()).toEqual({ data: '123', meta: [] }) - }) - - it('should handle request - lazy', async () => { - const response = await handler({ - router, - request: new Request('http://localhost/lazyRouter/ping', { - method: 'POST', - headers: { - [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ data: { value: '123' }, meta: [] }), - }), - }) - - expect(response?.status).toEqual(200) - expect(await response?.json()).toEqual({ data: '123', meta: [] }) - }) - - it('should handle request - lazy - lazy', async () => { - const response = await handler({ - router, - request: new Request('http://localhost/lazyRouter/lazyRouter/ping', { - method: 'POST', - headers: { - [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ data: { value: '123' }, meta: [] }), - }), - }) - - expect(response?.status).toEqual(200) - expect(await response?.json()).toEqual({ data: '123', meta: [] }) - }) - - it('should throw error - not found', async () => { - const response = await handler({ - router, - request: new Request('http://localhost/pingp', { - method: 'POST', - headers: { - [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE, - }, - body: JSON.stringify({ data: { value: '123' }, meta: [] }), - }), - }) - - expect(response?.status).toEqual(404) - expect(await response?.json()).toEqual({ data: { code: 'NOT_FOUND', message: 'Not found', status: 404 }, meta: [] }) - }) - - it('should throw error - not found - lazy', async () => { - const response = await handler({ - router, - request: new Request('http://localhost/lazyRouter/not_found', { - method: 'POST', - headers: { - [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE, - }, - body: JSON.stringify({ data: { value: '123' }, meta: [] }), - }), - }) - - expect(response?.status).toEqual(404) - expect(await response?.json()).toEqual({ data: { code: 'NOT_FOUND', message: 'Not found', status: 404 }, meta: [] }) - }) - - it('should throw error - invalid input', async () => { - const response = await handler({ - router, - request: new Request('http://localhost/ping', { - method: 'POST', - headers: { - [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE, - }, - body: JSON.stringify({ data: { value: 123 }, meta: [] }), - }), - }) - - expect(response?.status).toEqual(400) - expect(await response?.json()).toEqual({ - data: { - code: 'BAD_REQUEST', - status: 400, - message: 'Input validation failed', - issues: [ - { - code: 'invalid_type', - expected: 'string', - received: 'number', - path: [ - 'value', - ], - message: 'Expected string, received number', - }, - ], - }, - meta: [], - }) - }) - - it('should throw error - invalid input - lazy', async () => { - const response = await handler({ - router, - request: new Request('http://localhost/lazyRouter/ping', { - method: 'POST', - headers: { - [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE, - }, - body: JSON.stringify({ data: { value: 123 }, meta: [] }), - }), - }) - - expect(response?.status).toEqual(400) - expect(await response?.json()).toEqual({ - data: { - code: 'BAD_REQUEST', - status: 400, - message: 'Input validation failed', - issues: [ - { - code: 'invalid_type', - expected: 'string', - received: 'number', - path: [ - 'value', - ], - message: 'Expected string, received number', - }, - ], - }, - meta: [], - }) - }) - - it('should throw error - invalid input - lazy - lazy', async () => { - const response = await handler({ - router, - request: new Request('http://localhost/lazyRouter/lazyRouter/ping', { - method: 'POST', - headers: { - [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE, - }, - body: JSON.stringify({ data: { value: 123 }, meta: [] }), - }), - }) - - expect(response?.status).toEqual(400) - expect(await response?.json()).toEqual({ - data: { - code: 'BAD_REQUEST', - status: 400, - message: 'Input validation failed', - issues: [ - { - code: 'invalid_type', - expected: 'string', - received: 'number', - path: [ - 'value', - ], - message: 'Expected string, received number', - }, - ], - }, - meta: [], - }) - }) - - it('abort signal', async () => { - const controller = new AbortController() - const signal = controller.signal - - const func = vi.fn() - const onSuccess = vi.fn() - - const ping = os.func(func) - - const response = await handler({ - router: { ping }, - request: new Request('http://localhost/ping', { - method: 'POST', - headers: { - [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ data: { value: '123' }, meta: [] }), - }), - signal, - onSuccess, - }) - - expect(response?.status).toEqual(200) - expect(func).toBeCalledTimes(1) - expect(func.mock.calls[0]![2].signal).toBe(signal) - expect(onSuccess).toBeCalledTimes(1) - expect(func.mock.calls[0]![2].signal).toBe(signal) - }) -}) diff --git a/packages/server/src/fetch/index.ts b/packages/server/src/fetch/index.ts index f2899b2d..11433a67 100644 --- a/packages/server/src/fetch/index.ts +++ b/packages/server/src/fetch/index.ts @@ -1,3 +1,3 @@ export * from './handle-request' -export * from './handler' +export * from './orpc-handler' export * from './types' diff --git a/packages/server/src/fetch/orpc-handler.test-d.ts b/packages/server/src/fetch/orpc-handler.test-d.ts new file mode 100644 index 00000000..e9544444 --- /dev/null +++ b/packages/server/src/fetch/orpc-handler.test-d.ts @@ -0,0 +1,21 @@ +import { handleFetchRequest } from './handle-request' +import { createORPCHandler } from './orpc-handler' + +it('assignable to handlers', () => { + handleFetchRequest({ + request: new Request('https://example.com', {}), + router: {}, + handlers: [ + createORPCHandler(), + ], + }) + + handleFetchRequest({ + request: new Request('https://example.com', {}), + router: {}, + handlers: [ + // @ts-expect-error - invalid handler + createORPCHandler, + ], + }) +}) diff --git a/packages/server/src/fetch/orpc-handler.test.ts b/packages/server/src/fetch/orpc-handler.test.ts new file mode 100644 index 00000000..9034e289 --- /dev/null +++ b/packages/server/src/fetch/orpc-handler.test.ts @@ -0,0 +1,202 @@ +import { ContractProcedure } from '@orpc/contract' +import { ORPC_PROTOCOL_HEADER, ORPC_PROTOCOL_VALUE } from '@orpc/shared' +import { describe, expect, it, vi } from 'vitest' +import { lazy } from '../lazy' +import { Procedure } from '../procedure' +import { createProcedureCaller } from '../procedure-caller' +import { createORPCHandler } from './orpc-handler' + +vi.mock('../procedure-caller', () => ({ + createProcedureCaller: vi.fn(() => vi.fn()), +})) + +describe('createORPCHandler', () => { + const ping = new Procedure({ + contract: new ContractProcedure({ + InputSchema: undefined, + OutputSchema: undefined, + }), + func: vi.fn(), + }) + const pong = new Procedure({ + contract: new ContractProcedure({ + InputSchema: undefined, + OutputSchema: undefined, + }), + func: vi.fn(), + }) + + const router = { + ping: lazy(() => Promise.resolve({ default: ping })), + pong, + nested: lazy(() => Promise.resolve({ default: { + ping, + pong: lazy(() => Promise.resolve({ default: pong })), + } })), + } + + it('should return undefined if the protocol header is missing or incorrect', async () => { + const handler = createORPCHandler() + + const response = await handler({ + request: new Request('https://example.com', { + headers: new Headers({}), + }), + router, + context: undefined, + signal: undefined, + }) + + expect(response).toBeUndefined() + }) + + it('should return a 404 response if no matching procedure is found', async () => { + const handler = createORPCHandler() + + const mockRequest = new Request('https://example.com/not_found', { + headers: new Headers({ [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE }), + }) + + const response = await handler({ + request: mockRequest, + router, + context: undefined, + signal: undefined, + }) + + expect(response?.status).toBe(404) + + const body = await response?.text() + expect(body).toContain('Not found') + }) + + it('should return a 200 response with serialized output if procedure is resolved successfully', async () => { + const handler = createORPCHandler() + + const caller = vi.fn().mockReturnValueOnce('__mocked__') + vi.mocked(createProcedureCaller).mockReturnValue(caller) + + const mockRequest = new Request('https://example.com/ping', { + headers: new Headers({ [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE }), + method: 'POST', + body: JSON.stringify({ data: { value: '123' }, meta: [] }), + }) + + const response = await handler({ + request: mockRequest, + router, + }) + + expect(response?.status).toBe(200) + + const body = await response?.json() + expect(body).toEqual({ data: '__mocked__', meta: [] }) + + expect(caller).toBeCalledTimes(1) + expect(caller).toBeCalledWith({ value: '123' }, { signal: undefined }) + }) + + it('should handle deserialization errors and return a 400 response', async () => { + const handler = createORPCHandler() + + const mockRequest = new Request('https://example.com/ping', { + method: 'POST', + headers: new Headers({ [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE, 'Content-Type': 'application/json' }), + body: '{ invalid json', + }) + + const response = await handler({ + request: mockRequest, + router, + }) + + expect(response?.status).toBe(400) + + const body = await response?.text() + expect(body).toContain('Cannot parse request') + }) + + it('should handle unexpected errors and return a 500 response', async () => { + const handler = createORPCHandler() + + vi.mocked(createProcedureCaller).mockImplementationOnce(() => { + throw new Error('Unexpected error') + }) + + const mockRequest = new Request('https://example.com/ping', { + headers: new Headers({ [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE }), + method: 'POST', + body: JSON.stringify({ data: { value: '123' }, meta: [] }), + }) + + const response = await handler({ + request: mockRequest, + router, + context: undefined, + signal: undefined, + }) + + expect(response?.status).toBe(500) + + const body = await response?.text() + expect(body).toContain('Internal server error') + }) + + it('support signal', async () => { + const handler = createORPCHandler() + + const caller = vi.fn().mockReturnValueOnce('__mocked__') + vi.mocked(createProcedureCaller).mockReturnValue(caller) + + const mockRequest = new Request('https://example.com/ping', { + headers: new Headers({ [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE }), + method: 'POST', + body: JSON.stringify({ data: { value: '123' }, meta: [] }), + }) + + const controller = new AbortController() + const signal = controller.signal + + const response = await handler({ + request: mockRequest, + router, + signal, + }) + + expect(response?.status).toBe(200) + + const body = await response?.json() + expect(body).toEqual({ data: '__mocked__', meta: [] }) + + expect(caller).toBeCalledTimes(1) + expect(caller).toBeCalledWith({ value: '123' }, { signal }) + }) + + it('hooks', async () => { + const handler = createORPCHandler() + + const mockRequest = new Request('https://example.com/not_found', { + headers: new Headers({ [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE }), + method: 'POST', + body: JSON.stringify({ data: { value: '123' }, meta: [] }), + }) + + const onStart = vi.fn() + const onSuccess = vi.fn() + const onError = vi.fn() + + const response = await handler({ + request: mockRequest, + router, + onStart, + onSuccess, + onError, + }) + + expect(response?.status).toBe(404) + + expect(onStart).toBeCalledTimes(1) + expect(onSuccess).toBeCalledTimes(0) + expect(onError).toBeCalledTimes(1) + }) +}) diff --git a/packages/server/src/fetch/handler.ts b/packages/server/src/fetch/orpc-handler.ts similarity index 60% rename from packages/server/src/fetch/handler.ts rename to packages/server/src/fetch/orpc-handler.ts index 7a564ad6..e88d784d 100644 --- a/packages/server/src/fetch/handler.ts +++ b/packages/server/src/fetch/orpc-handler.ts @@ -1,12 +1,12 @@ import type { ANY_LAZY_PROCEDURE, ANY_PROCEDURE } from '../procedure' -import type { Router } from '../router' import type { FetchHandler } from './types' import { executeWithHooks, ORPC_PROTOCOL_HEADER, ORPC_PROTOCOL_VALUE, trim, value } from '@orpc/shared' import { ORPCError } from '@orpc/shared/error' import { ORPCDeserializer, ORPCSerializer } from '@orpc/transformer' -import { isLazy } from '../lazy' +import { unlazy } from '../lazy' import { isProcedure } from '../procedure' import { createProcedureCaller } from '../procedure-caller' +import { type ANY_ROUTER, getRouterChild } from '../router' const serializer = new ORPCSerializer() const deserializer = new ORPCDeserializer() @@ -23,17 +23,17 @@ export function createORPCHandler(): FetchHandler { const url = new URL(options.request.url) const pathname = `/${trim(url.pathname.replace(options.prefix ?? '', ''), '/')}` - const match = resolveORPCRouter(options.router, pathname) + const match = await resolveRouterMatch(options.router, pathname) if (!match) { throw new ORPCError({ code: 'NOT_FOUND', message: 'Not found' }) } - const input = await deserializeRequest(options.request) + const input = await parseRequestInput(options.request) const caller = createProcedureCaller({ context, - procedure: match.procedure as any, + procedure: match.procedure, path: match.path, }) @@ -58,58 +58,60 @@ export function createORPCHandler(): FetchHandler { }, }) } - catch (e) { - const error = e instanceof ORPCError - ? e - : new ORPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: 'Internal server error', - cause: e, - }) - - const { body, headers } = serializer.serialize(error.toJSON()) - - return new Response(body, { - status: error.status, - headers, - }) + catch (error) { + return handleErrorResponse(error) } } } -function resolveORPCRouter(router: Router, pathname: string): { +async function resolveRouterMatch( + router: ANY_ROUTER, + pathname: string, +): Promise<{ path: string[] procedure: ANY_PROCEDURE | ANY_LAZY_PROCEDURE -} | undefined { - const path = trim(pathname, '/').split('/').map(decodeURIComponent) - - let current: Router | ANY_PROCEDURE | ANY_LAZY_PROCEDURE | undefined = router - for (const segment of path) { - if ((typeof current !== 'object' && typeof current !== 'function') || !current) { - current = undefined - break - } +} | undefined> { + const pathSegments = trim(pathname, '/').split('/').map(decodeURIComponent) + + const match = getRouterChild(router, ...pathSegments) + const { default: maybeProcedure } = await unlazy(match) - current = (current as any)[segment] + if (!isProcedure(maybeProcedure)) { + return undefined } - return isProcedure(current) || isLazy(current) - ? { - procedure: current, - path, - } - : undefined + return { + procedure: maybeProcedure, + path: pathSegments, + } } -async function deserializeRequest(request: Request): Promise { +async function parseRequestInput(request: Request): Promise { try { return await deserializer.deserialize(request) } - catch (e) { + catch (error) { throw new ORPCError({ code: 'BAD_REQUEST', message: 'Cannot parse request. Please check the request body and Content-Type header.', - cause: e, + cause: error, }) } } + +function handleErrorResponse(error: unknown): Response { + const orpcError = error instanceof ORPCError + ? error + : new ORPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Internal server error', + cause: error, + }) + + const { body, headers } = serializer.serialize(orpcError.toJSON()) + + return new Response(body, { + status: orpcError.status, + headers, + }) +} diff --git a/packages/server/src/fetch/types.ts b/packages/server/src/fetch/types.ts index f1dc8a1f..0e810ac0 100644 --- a/packages/server/src/fetch/types.ts +++ b/packages/server/src/fetch/types.ts @@ -1,5 +1,6 @@ /// +import type { HTTPPath } from '@orpc/contract' import type { Hooks, Value } from '@orpc/shared' import type { Router } from '../router' import type { CallerOptions, Context } from '../types' @@ -23,7 +24,7 @@ export type FetchHandlerOptions = * @example /orpc * @example /api */ - prefix?: string + prefix?: HTTPPath } & NoInfer<(undefined extends T ? { context?: Value } : { context: Value })> & CallerOptions diff --git a/packages/server/src/procedure-caller.test.ts b/packages/server/src/procedure-caller.test.ts index 43f5bda6..3715f47e 100644 --- a/packages/server/src/procedure-caller.test.ts +++ b/packages/server/src/procedure-caller.test.ts @@ -295,16 +295,6 @@ describe.each(procedureCases)('createProcedureCaller - case %s', async (_, proce }) }) -it('should throw error when invalid lazy procedure', () => { - const lazied = lazy(() => Promise.resolve({ default: 123 })) - - const caller = createProcedureCaller({ - procedure: lazied, - }) - - expect(caller()).rejects.toThrow('Not found') -}) - it('still work without middleware', async () => { const procedure = new Procedure({ contract: new ContractProcedure({ diff --git a/packages/server/src/procedure-caller.ts b/packages/server/src/procedure-caller.ts index b7d9b014..4aa6296a 100644 --- a/packages/server/src/procedure-caller.ts +++ b/packages/server/src/procedure-caller.ts @@ -1,19 +1,16 @@ import type { Schema, SchemaInput, SchemaOutput } from '@orpc/contract' import type { Hooks, Value } from '@orpc/shared' -import type { Lazy, Lazyable } from './lazy' +import type { Lazyable } from './lazy' import type { MiddlewareMeta } from './middleware' -import type { Caller, Context, Meta, WELL_CONTEXT } from './types' +import type { + ANY_PROCEDURE, + Procedure, +} from './procedure' -import { executeWithHooks, trim, value } from '@orpc/shared' +import type { Caller, Context, Meta, WELL_CONTEXT } from './types' +import { executeWithHooks, value } from '@orpc/shared' import { ORPCError } from '@orpc/shared/error' -import { isLazy, unlazy } from './lazy' -import { - type ANY_LAZY_PROCEDURE, - type ANY_PROCEDURE, - isProcedure, - type Procedure, - type WELL_PROCEDURE, -} from './procedure' +import { unlazy } from './lazy' import { mergeContext } from './utils' /** @@ -26,9 +23,7 @@ export type CreateProcedureCallerOptions< TFuncOutput extends SchemaInput, > = & { - procedure: - | Lazyable> - | Lazy + procedure: Lazyable> /** * This is helpful for logging and analytics. @@ -55,7 +50,7 @@ export function createProcedureCaller< ): Caller, SchemaOutput> { return async (...[input, callerOptions]) => { const path = options.path ?? [] - const procedure = await loadProcedure(options.procedure) as WELL_PROCEDURE + const { default: procedure } = await unlazy(options.procedure) const context = await value(options.context) as TContext const meta: Meta = { @@ -87,7 +82,7 @@ export function createProcedureCaller< } } -async function validateInput(procedure: WELL_PROCEDURE, input: unknown) { +async function validateInput(procedure: ANY_PROCEDURE, input: unknown) { const schema = procedure['~orpc'].contract['~orpc'].InputSchema if (!schema) return input @@ -104,7 +99,7 @@ async function validateInput(procedure: WELL_PROCEDURE, input: unknown) { return result.value } -async function validateOutput(procedure: WELL_PROCEDURE, output: unknown) { +async function validateOutput(procedure: ANY_PROCEDURE, output: unknown) { const schema = procedure['~orpc'].contract['~orpc'].OutputSchema if (!schema) return output @@ -122,7 +117,7 @@ async function validateOutput(procedure: WELL_PROCEDURE, output: unknown) { } async function executeMiddlewareChain( - procedure: WELL_PROCEDURE, + procedure: ANY_PROCEDURE, input: unknown, context: Context, meta: Meta, @@ -154,22 +149,3 @@ async function executeMiddlewareChain( return (await next({})).output } - -export async function loadProcedure(procedure: ANY_PROCEDURE | ANY_LAZY_PROCEDURE | Lazy): Promise { - const loadedProcedure = isLazy(procedure) - ? (await unlazy(procedure)).default - : procedure - - if (!isProcedure(loadedProcedure)) { - throw new ORPCError({ - code: 'NOT_FOUND', - message: 'Not found', - cause: new Error(trim(` - Attempted to call a lazy router or invalid procedure. - This should typically be caught by TypeScript compilation. - `)), - }) - } - - return loadedProcedure -} diff --git a/packages/server/src/router-caller.test.ts b/packages/server/src/router-caller.test.ts index 14d1e3bf..67f4e9f2 100644 --- a/packages/server/src/router-caller.test.ts +++ b/packages/server/src/router-caller.test.ts @@ -1,6 +1,6 @@ import { ContractProcedure } from '@orpc/contract' import { z } from 'zod' -import { lazy, unlazy } from './lazy' +import { isLazy, lazy, unlazy } from './lazy' import { Procedure } from './procedure' import { createProcedureCaller } from './procedure-caller' import { createRouterCaller } from './router-caller' @@ -147,4 +147,41 @@ describe('createRouterCaller', () => { it('not recursive on symbol', () => { expect((caller as any)[Symbol('something')]).toBeUndefined() }) + + it('throw error if call on invalid lazy', async () => { + const caller = createRouterCaller({ + router: lazy(() => Promise.resolve({ default: undefined })), + }) + + // @ts-expect-error --- invalid lazy + caller.router.ping.pong({ val: '123' }) + + const procedure = vi.mocked(createProcedureCaller).mock.calls[0]![0].procedure + + expect(procedure).toSatisfy(isLazy) + + expect(unlazy(procedure)).rejects.toThrow('Expected a valid procedure or lazy but got unknown.') + }) + + it('return undefined if access the undefined key', async () => { + const caller = createRouterCaller({ + router: { + ping, + }, + }) + + // @ts-expect-error --- invalid access + expect(caller.router).toBeUndefined() + }) + + it('works without base path', async () => { + const caller = createRouterCaller({ + router: { + ping, + }, + }) + + expect(caller.ping({ val: '123' })).toEqual('__mocked__') + expect(vi.mocked(createProcedureCaller).mock.calls[0]![0].path).toEqual(['ping']) + }) }) diff --git a/packages/server/src/router-caller.ts b/packages/server/src/router-caller.ts index 72f47072..461cc87a 100644 --- a/packages/server/src/router-caller.ts +++ b/packages/server/src/router-caller.ts @@ -3,7 +3,7 @@ import type { Hooks, Value } from '@orpc/shared' import type { Lazy } from './lazy' import type { Procedure } from './procedure' import type { Caller, Meta } from './types' -import { isLazy } from './lazy' +import { isLazy, lazy, unlazy } from './lazy' import { isProcedure } from './procedure' import { createProcedureCaller } from './procedure-caller' import { type ANY_ROUTER, getRouterChild, type Router } from './router' @@ -53,7 +53,19 @@ export function createRouterCaller< const procedureCaller = isLazy(options.router) ? createProcedureCaller({ ...options, - procedure: options.router, + procedure: lazy(async () => { + const { default: maybeProcedure } = await unlazy(options.router) + + if (!isProcedure(maybeProcedure)) { + throw new Error(` + Expected a valid procedure or lazy but got unknown. + This should be caught by TypeScript compilation. + Please report this issue if this makes you feel uncomfortable. + `) + } + + return { default: maybeProcedure } + }), context: options.context, path: options.path, }) diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index 95d06374..ab2936e0 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -1,6 +1,6 @@ /// -import type { WELL_PROCEDURE } from './procedure' +import type { ANY_PROCEDURE } from './procedure' export type Context = Record | undefined export type WELL_CONTEXT = Record | undefined @@ -20,5 +20,5 @@ export interface Caller { export interface Meta extends CallerOptions { path: string[] - procedure: WELL_PROCEDURE + procedure: ANY_PROCEDURE } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46068357..4b9a0353 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -325,9 +325,6 @@ importers: specifier: workspace:* version: link:../zod devDependencies: - '@orpc/openapi': - specifier: workspace:* - version: link:../openapi zod: specifier: ^3.24.1 version: 3.24.1