From 595db2f8254d891f81331b19c01285f4b23f158e Mon Sep 17 00:00:00 2001 From: unnoq Date: Mon, 2 Dec 2024 10:35:16 +0700 Subject: [PATCH 01/11] wip - apis --- packages/client/src/procedure.test.ts | 9 +- packages/client/src/router.test.ts | 5 +- packages/react/tests/orpc.tsx | 3 +- packages/server/package.json | 3 - packages/server/src/adapters/fetch.test.ts | 125 ++++---- packages/server/src/adapters/fetch.ts | 328 ++++++++------------- pnpm-lock.yaml | 10 - 7 files changed, 191 insertions(+), 292 deletions(-) diff --git a/packages/client/src/procedure.test.ts b/packages/client/src/procedure.test.ts index 953339210..af000215f 100644 --- a/packages/client/src/procedure.test.ts +++ b/packages/client/src/procedure.test.ts @@ -1,5 +1,4 @@ import { ORPCError, os } from '@orpc/server' -import { createFetchHandler } from '@orpc/server/fetch' import { z } from 'zod' import { createProcedureClient } from './procedure' @@ -14,7 +13,7 @@ describe('createProcedureClient', () => { ping, }, }) - const handler = createFetchHandler({ + const handler = handleFetchRequest({ router, }) const orpcFetch: typeof fetch = async (...args) => { @@ -114,7 +113,7 @@ describe('createProcedureClient', () => { .func(input => input.value), }) - const handler = createFetchHandler({ + const handler = handleFetchRequest({ router, }) @@ -147,7 +146,7 @@ describe('createProcedureClient', () => { }), }) - const handler = createFetchHandler({ + const handler = handleFetchRequest({ router, }) @@ -192,7 +191,7 @@ describe('upload file', () => { }), }) - const handler = createFetchHandler({ router }) + const handler = handleFetchRequest({ router }) const orpcFetch: typeof fetch = async (...args) => { const request = new Request(...args) diff --git a/packages/client/src/router.test.ts b/packages/client/src/router.test.ts index e17f1a093..f79ceb153 100644 --- a/packages/client/src/router.test.ts +++ b/packages/client/src/router.test.ts @@ -1,6 +1,5 @@ import { oc } from '@orpc/contract' import { os } from '@orpc/server' -import { createFetchHandler } from '@orpc/server/fetch' import { z } from 'zod' import { createRouterClient } from './router' @@ -15,7 +14,7 @@ describe('createRouterClient', () => { unique: ping, }, }) - const handler = createFetchHandler({ + const handler = handleFetchRequest({ router, }) const orpcFetch: typeof fetch = async (...args) => { @@ -126,7 +125,7 @@ describe('createRouterClient', () => { .func(input => input.value), }) - const handler = createFetchHandler({ + const handler = handleFetchRequest({ router, }) diff --git a/packages/react/tests/orpc.tsx b/packages/react/tests/orpc.tsx index 57c20cd44..327679dd2 100644 --- a/packages/react/tests/orpc.tsx +++ b/packages/react/tests/orpc.tsx @@ -1,6 +1,5 @@ import { createORPCClient } from '@orpc/client' import { os } from '@orpc/server' -import { createFetchHandler } from '@orpc/server/fetch' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import React, { Suspense } from 'react' import { z } from 'zod' @@ -92,7 +91,7 @@ export const appRouter = orpcServer.router({ }, }) -export const appHandler = createFetchHandler({ router: appRouter }) +export const appHandler = handleFetchRequest({ router: appRouter }) export const orpcClient = createORPCClient({ baseURL: 'http://localhost:3000', diff --git a/packages/server/package.json b/packages/server/package.json index a3a021ba7..396835d4a 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -54,8 +54,5 @@ "@orpc/contract": "workspace:*", "@orpc/shared": "workspace:*", "@orpc/transformer": "workspace:*" - }, - "devDependencies": { - "hono": "^4.6.3" } } diff --git a/packages/server/src/adapters/fetch.test.ts b/packages/server/src/adapters/fetch.test.ts index c4e3a6103..653f3af92 100644 --- a/packages/server/src/adapters/fetch.test.ts +++ b/packages/server/src/adapters/fetch.test.ts @@ -3,7 +3,7 @@ import { oz } from '@orpc/zod' import { describe, expect, it } from 'vitest' import { z } from 'zod' import { ORPCError, os } from '..' -import { createFetchHandler } from './fetch' +import { handleFetchRequest } from './fetch' const router = os.router({ throw: os.func(() => { @@ -17,8 +17,6 @@ const router = os.router({ }), }) -const handler = createFetchHandler({ router }) - describe('simple', () => { const osw = os.context<{ auth?: boolean }>() const router = osw.router({ @@ -27,12 +25,10 @@ describe('simple', () => { .route({ method: 'GET', path: '/ping2' }) .func(async () => 'pong2'), }) - const handler = createFetchHandler({ - router, - }) it('200: public url', async () => { - const response = await handler({ + const response = await handleFetchRequest({ + router, prefix: '/orpc', request: new Request('http://localhost/orpc/ping', { method: 'POST', @@ -43,7 +39,8 @@ describe('simple', () => { expect(response.status).toBe(200) expect(await response.json()).toEqual('pong') - const response2 = await handler({ + const response2 = await handleFetchRequest({ + router, prefix: '/orpc', request: new Request('http://localhost/orpc/ping2', { method: 'GET', @@ -56,7 +53,8 @@ describe('simple', () => { }) it('200: internal url', async () => { - const response = await handler({ + const response = await handleFetchRequest({ + router, request: new Request('http://localhost/ping'), context: { auth: true }, }) @@ -64,7 +62,8 @@ describe('simple', () => { expect(response.status).toBe(200) expect(await response.json()).toEqual('pong') - const response2 = await handler({ + const response2 = await handleFetchRequest({ + router, prefix: '/orpc', request: new Request('http://localhost/orpc/ping2'), context: { auth: true }, @@ -75,7 +74,8 @@ describe('simple', () => { }) it('404', async () => { - const response = await handler({ + const response = await handleFetchRequest({ + router, prefix: '/orpc', request: new Request('http://localhost/orpc/pingp', { method: 'POST', @@ -89,7 +89,8 @@ describe('simple', () => { describe('procedure throw error', () => { it('unknown error', async () => { - const response = await handler({ + const response = await handleFetchRequest({ + router, request: new Request('https://local.com/throw', { method: 'POST' }), }) @@ -108,9 +109,8 @@ describe('procedure throw error', () => { }), }) - const handler = createFetchHandler({ router }) - - const response = await handler({ + const response = await handleFetchRequest({ + router, request: new Request('https://local.com/ping', { method: 'POST' }), }) @@ -133,9 +133,8 @@ describe('procedure throw error', () => { }), }) - const handler = createFetchHandler({ router }) - - const response = await handler({ + const response = await handleFetchRequest({ + router, request: new Request('https://local.com/ping', { method: 'POST' }), }) @@ -165,9 +164,8 @@ describe('procedure throw error', () => { }), }) - const handler = createFetchHandler({ router }) - - const response = await handler({ + const response = await handleFetchRequest({ + router, request: new Request('https://local.com/ping', { method: 'POST' }), }) @@ -178,7 +176,8 @@ describe('procedure throw error', () => { message: 'Internal server error', }) - const response2 = await handler({ + const response2 = await handleFetchRequest({ + router, request: new Request('https://local.com/ping2', { method: 'POST' }), }) @@ -200,9 +199,8 @@ describe('procedure throw error', () => { }), }) - const handler = createFetchHandler({ router }) - - const response = await handler({ + const response = await handleFetchRequest({ + router, request: new Request('https://local.com/ping', { method: 'POST' }), }) @@ -233,9 +231,8 @@ describe('procedure throw error', () => { }), }) - const handler = createFetchHandler({ router }) - - const response = await handler({ + const response = await handleFetchRequest({ + router, request: new Request('https://local.com/ping', { method: 'POST', body: '"hi"', @@ -257,7 +254,8 @@ describe('hooks', () => { const onError = vi.fn() const onFinish = vi.fn() - const handler = createFetchHandler({ + await handleFetchRequest({ + prefix: '/orpc', router, hooks: async (context, hooks) => { try { @@ -273,10 +271,6 @@ describe('hooks', () => { onFinish() } }, - }) - - await handler({ - prefix: '/orpc', request: new Request('http://localhost/orpc/ping', { method: 'POST', }), @@ -295,7 +289,7 @@ describe('hooks', () => { const onError = vi.fn() const onFinish = vi.fn() - const handler = createFetchHandler({ + await handleFetchRequest({ router, hooks: async (context, hooks) => { try { @@ -311,9 +305,6 @@ describe('hooks', () => { onFinish() } }, - }) - - await handler({ prefix: '/orpc', request: new Request('http://localhost/orpc/throw', { method: 'POST', @@ -344,8 +335,6 @@ describe('file upload', () => { }), }) - const handler = createFetchHandler({ router }) - const blob1 = new Blob(['hello'], { type: 'text/plain;charset=utf-8' }) const blob2 = new Blob(['"world"'], { type: 'image/png' }) const blob3 = new Blob(['unnoq'], { type: 'application/octet-stream' }) @@ -356,7 +345,8 @@ describe('file upload', () => { rForm.set('maps', JSON.stringify([[]])) rForm.set('0', blob3) - const response = await handler({ + const response = await handleFetchRequest({ + router, prefix: '/orpc', request: new Request('http://localhost/orpc/signal', { method: 'POST', @@ -385,7 +375,8 @@ describe('file upload', () => { form.set('0', blob1) form.set('1', blob2) - const response = await handler({ + const response = await handleFetchRequest({ + router, prefix: '/orpc', request: new Request('http://localhost/orpc/multiple', { method: 'POST', @@ -418,12 +409,10 @@ describe('accept header', () => { const router = os.router({ ping: os.func(async () => 'pong'), }) - const handler = createFetchHandler({ - router, - }) it('application/json', async () => { - const response = await handler({ + const response = await handleFetchRequest({ + router, prefix: '/orpc', request: new Request('http://localhost/orpc/ping', { method: 'POST', @@ -439,7 +428,8 @@ describe('accept header', () => { }) it('multipart/form-data', async () => { - const response = await handler({ + const response = await handleFetchRequest({ + router, prefix: '/orpc', request: new Request('http://localhost/orpc/ping', { method: 'POST', @@ -458,7 +448,8 @@ describe('accept header', () => { }) it('application/x-www-form-urlencoded', async () => { - const response = await handler({ + const response = await handleFetchRequest({ + router, prefix: '/orpc', request: new Request('http://localhost/orpc/ping', { method: 'POST', @@ -477,7 +468,8 @@ describe('accept header', () => { }) it('*/*', async () => { - const response = await handler({ + const response = await handleFetchRequest({ + router, prefix: '/orpc', request: new Request('http://localhost/orpc/ping', { method: 'POST', @@ -492,7 +484,8 @@ describe('accept header', () => { }) it('invalid', async () => { - const response = await handler({ + const response = await handleFetchRequest({ + router, prefix: '/orpc', request: new Request('http://localhost/orpc/ping', { method: 'POST', @@ -541,17 +534,13 @@ describe('dynamic params', () => { }) const handlers = [ - createFetchHandler({ - router, - }), - createFetchHandler({ - router, - serverless: true, - }), + { router }, + { router }, // TODO: to test for serverless ] - it.each(handlers)('should handle dynamic params', async (handler) => { - const response = await handler({ + it.each(handlers)('should handle dynamic params', async ({ router }) => { + const response = await handleFetchRequest({ + router, request: new Request('http://localhost/123'), }) @@ -564,7 +553,8 @@ describe('dynamic params', () => { const form = new FormData() form.append('file', new Blob(['hello']), 'hello.txt') - const response = await handler({ + const response = await handleFetchRequest({ + router, request: new Request('http://localhost/123/dfdsfds', { method: 'POST', body: form, @@ -595,20 +585,16 @@ describe('can control method on POST request', () => { }) const handlers = [ - createFetchHandler({ - router, - }), - createFetchHandler({ - router, - serverless: true, - }), + { router }, + { router }, // TODO: to test for serverless ] - it.each(handlers)('work', async (handler) => { + it.each(handlers)('work', async ({ router }) => { const form = new FormData() form.set('file', new File(['hello'], 'hello.txt')) - const response = await handler({ + const response = await handleFetchRequest({ + router, request: new Request('http://localhost/123', { method: 'POST', body: form, @@ -617,7 +603,8 @@ describe('can control method on POST request', () => { expect(response.status).toEqual(404) - const response2 = await handler({ + const response2 = await handleFetchRequest({ + router, request: new Request('http://localhost/123?method=PUT', { method: 'POST', body: form, diff --git a/packages/server/src/adapters/fetch.ts b/packages/server/src/adapters/fetch.ts index ce1e66958..2f959b79d 100644 --- a/packages/server/src/adapters/fetch.ts +++ b/packages/server/src/adapters/fetch.ts @@ -5,17 +5,14 @@ import type { Promisable, Value, } from '@orpc/shared' +import type { Procedure, WELL_DEFINED_PROCEDURE } from '../procedure' import type { Router } from '../router' import { - type HTTPPath, ORPC_HEADER, ORPC_HEADER_VALUE, - standardizeHTTPPath, } from '@orpc/contract' import { - get, isPlainObject, - mapValues, trim, value, } from '@orpc/shared' @@ -27,9 +24,7 @@ import { ORPCSerializer, zodCoerce, } from '@orpc/transformer' -import { LinearRouter } from 'hono/router/linear-router' -import { RegExpRouter } from 'hono/router/reg-exp-router' -import { isProcedure, type WELL_DEFINED_PROCEDURE } from '../procedure' +import { isProcedure } from '../procedure' import { createProcedureCaller } from '../procedure-caller' export interface FetchHandlerHooks { @@ -37,9 +32,25 @@ export interface FetchHandlerHooks { response: (response: Response) => Response } -export interface CreateFetchHandlerOptions> { +export type HandleFetchRequestOptions> = { + /** + * The router used to handle the request. + */ router: TRouter + /** + * The request need to be handled. + */ + request: Request + + /** + * Remove the prefix from the request path. + * + * @example /orpc + * @example /api + */ + prefix?: string + /** * Hooks for executing logics on lifecycle events. */ @@ -47,238 +58,155 @@ export interface CreateFetchHandlerOptions> { context: TRouter extends Router ? UContext : never, hooks: FetchHandlerHooks, ) => Promisable - +} & PartialOnUndefinedDeep<{ /** - * It will help improve the cold start time. But it will increase the performance. - * - * @default false + * The context used to handle the request. */ - serverless?: boolean -} + context: Value< + TRouter extends Router ? UContext : never + > +}> -export function createFetchHandler>( - options: CreateFetchHandlerOptions, -): FetchHandler { - const routing = options.serverless - ? new LinearRouter<[string[], WELL_DEFINED_PROCEDURE]>() - : new RegExpRouter<[string[], WELL_DEFINED_PROCEDURE]>() +export async function handleFetchRequest>( + options: HandleFetchRequestOptions, +): Promise { + const isORPCTransformer + = options.request.headers.get(ORPC_HEADER) === ORPC_HEADER_VALUE + const accept = options.request.headers.get('Accept') || undefined - const addRouteRecursively = (router: Router, basePath: string[]) => { - for (const key in router) { - const currentPath = [...basePath, key] - const item = router[key] as WELL_DEFINED_PROCEDURE | Router + const serializer = isORPCTransformer + ? new ORPCSerializer() + : new OpenAPISerializer({ accept }) - if (isProcedure(item)) { - if (item.zz$p.contract.zz$cp.path) { - const method = item.zz$p.contract.zz$cp.method ?? 'POST' - const path = openAPIPathToRouterPath(item.zz$p.contract.zz$cp.path) + const context = await value(options.context) - routing.add(method, path, [currentPath, item]) - } - } - else { - addRouteRecursively(item, currentPath) - } - } - } + const handler = async () => { + const url = new URL(options.request.url) + const pathname = `/${trim(url.pathname.replace(options.prefix ?? '', ''), '/')}` - addRouteRecursively(options.router, []) + let path: string[] | undefined + let procedure: WELL_DEFINED_PROCEDURE | undefined + let params: Record | undefined - return async (requestOptions) => { - const isORPCTransformer - = requestOptions.request.headers.get(ORPC_HEADER) === ORPC_HEADER_VALUE - const accept = requestOptions.request.headers.get('Accept') || undefined + if (isORPCTransformer) { + path = trim(pathname, '/').split('/').map(decodeURIComponent) - const serializer = isORPCTransformer - ? new ORPCSerializer() - : new OpenAPISerializer({ accept }) + let current: Router | Procedure | undefined = options.router + for (const segment of path) { + if ((typeof current !== 'object' || current === null) && typeof current !== 'function') { + current = undefined + break + } - const context = await value(requestOptions.context) + current = (current as any)[segment] + } - const handler = async () => { - const url = new URL(requestOptions.request.url) - const pathname = `/${trim(url.pathname.replace(requestOptions.prefix ?? '', ''), '/')}` + if (isProcedure(current)) { + procedure = current + } + } + else { + // TODO + } - let path: string[] | undefined - let procedure: WELL_DEFINED_PROCEDURE | undefined - let params: Record | undefined + if (!path || !procedure) { + throw new ORPCError({ code: 'NOT_FOUND', message: 'Not found' }) + } - if (isORPCTransformer) { - path = trim(pathname, '/').split('/').map(decodeURIComponent) - const val = get(options.router, path) + const deserializer = isORPCTransformer + ? new ORPCDeserializer() + : new OpenAPIDeserializer({ + schema: procedure.zz$p.contract.zz$cp.InputSchema, + }) - if (isProcedure(val)) { - procedure = val - } + const input_ = await (async () => { + try { + return await deserializer.deserialize(options.request) } - else { - const customMethod - = requestOptions.request.method === 'POST' - ? url.searchParams.get('method')?.toUpperCase() - : undefined - const method = customMethod || requestOptions.request.method - - const [matches, params_] = routing.match(method, pathname) - - const [match] = matches.sort((a, b) => { - return Object.keys(a[1]).length - Object.keys(b[1]).length + catch (e) { + throw new ORPCError({ + code: 'BAD_REQUEST', + message: + 'Cannot parse request. Please check the request body and Content-Type header.', + cause: e, }) - - if (match) { - path = match[0][0] - procedure = match[0][1] - - if (params_) { - params = mapValues( - (match as any)[1]!, - v => params_[v as number]!, - ) - } - else { - params = match[1] as Record - } - } - - if (!path || !procedure) { - path = trim(pathname, '/').split('/').map(decodeURIComponent) - - const val = get(options.router, path) - - if (isProcedure(val)) { - procedure = val - } - } } + })() - if (!path || !procedure) { - throw new ORPCError({ code: 'NOT_FOUND', message: 'Not found' }) + const input = (() => { + if (!params || Object.keys(params).length === 0) { + return input_ } - const deserializer = isORPCTransformer - ? new ORPCDeserializer() - : new OpenAPIDeserializer({ - schema: procedure.zz$p.contract.zz$cp.InputSchema, - }) + const coercedParams = procedure.zz$p.contract.zz$cp.InputSchema + ? (zodCoerce( + procedure.zz$p.contract.zz$cp.InputSchema, + { ...params }, + { + bracketNotation: true, + }, + ) as object) + : params + + if (!isPlainObject(input_)) { + return coercedParams + } - const input_ = await (async () => { - try { - return await deserializer.deserialize(requestOptions.request) - } - catch (e) { - throw new ORPCError({ - code: 'BAD_REQUEST', - message: - 'Cannot parse request. Please check the request body and Content-Type header.', - cause: e, - }) - } - })() + return { + ...coercedParams, + ...input_, + } + })() - const input = (() => { - if (!params || Object.keys(params).length === 0) { - return input_ - } + const caller = createProcedureCaller({ + context, + procedure, + path, + }) - const coercedParams = procedure.zz$p.contract.zz$cp.InputSchema - ? (zodCoerce( - procedure.zz$p.contract.zz$cp.InputSchema, - { ...params }, - { - bracketNotation: true, - }, - ) as object) - : params - - if (!isPlainObject(input_)) { - return coercedParams - } + const output = await caller(input) - return { - ...coercedParams, - ...input_, - } - })() + const { body, headers } = serializer.serialize(output) - const caller = createProcedureCaller({ - context, - procedure, - path, - }) + return new Response(body, { + status: 200, + headers, + }) + } - const output = await caller(input) + try { + return await options.hooks?.(context as any, { + next: handler, + response: response => response, + }) ?? await handler() + } + catch (e) { + const error = toORPCError(e) - const { body, headers } = serializer.serialize(output) + try { + const { body, headers } = serializer.serialize(error.toJSON()) return new Response(body, { - status: 200, + status: error.status, headers, }) } - - try { - return await options.hooks?.(context as any, { - next: handler, - response: response => response, - }) ?? await handler() - } catch (e) { const error = toORPCError(e) - try { - const { body, headers } = serializer.serialize(error.toJSON()) - - return new Response(body, { - status: error.status, - headers, - }) - } - catch (e) { - const error = toORPCError(e) + // fallback to OpenAPI serializer (without accept) when expected serializer has failed + const { body, headers } = new OpenAPISerializer().serialize( + error.toJSON(), + ) - // fallback to OpenAPI serializer (without accept) when expected serializer has failed - const { body, headers } = new OpenAPISerializer().serialize( - error.toJSON(), - ) - - return new Response(body, { - status: error.status, - headers, - }) - } + return new Response(body, { + status: error.status, + headers, + }) } } } -function openAPIPathToRouterPath(path: HTTPPath): string { - return standardizeHTTPPath(path).replace(/\{([^}]+)\}/g, ':$1') -} - -export type FetchHandlerOptions> = { - /** - * The request need to be handled. - */ - request: Request - - /** - * Remove the prefix from the request path. - * - * @example /orpc - * @example /api - */ - prefix?: string -} & PartialOnUndefinedDeep<{ - /** - * The context used to handle the request. - */ - context: Value< - TRouter extends Router ? UContext : never - > -}> - -export interface FetchHandler> { - (options: FetchHandlerOptions): Promise -} - function toORPCError(e: unknown): ORPCError { return e instanceof ORPCError ? e diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 711a8d2c6..3ca992c1d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -246,10 +246,6 @@ importers: zod: specifier: '>=3.23.0' version: 3.23.8 - devDependencies: - hono: - specifier: ^4.6.3 - version: 4.6.6 packages/shared: dependencies: @@ -3239,10 +3235,6 @@ packages: hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} - hono@4.6.6: - resolution: {integrity: sha512-euUj5qwvtkG+p38GFs0LYacwaoS2hYRAGn9ysAggiwT2QBcPnT1XYUCW3hatW4C1KzAXTYuQ08BlVDJtAGuhlg==} - engines: {node: '>=16.9.0'} - hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -8404,8 +8396,6 @@ snapshots: dependencies: '@types/hast': 3.0.4 - hono@4.6.6: {} - hosted-git-info@2.8.9: {} html-encoding-sniffer@4.0.0: From 4080b9d12024e28193ef9c71396ed2cc905d3964 Mon Sep 17 00:00:00 2001 From: unnoq Date: Mon, 2 Dec 2024 13:53:46 +0700 Subject: [PATCH 02/11] wip --- packages/server/src/adapters/fetch.ts | 150 +++++++++++++------------- packages/server/src/index.ts | 1 + packages/server/src/routing.ts | 57 ++++++++++ 3 files changed, 131 insertions(+), 77 deletions(-) create mode 100644 packages/server/src/routing.ts diff --git a/packages/server/src/adapters/fetch.ts b/packages/server/src/adapters/fetch.ts index 2f959b79d..a3424f347 100644 --- a/packages/server/src/adapters/fetch.ts +++ b/packages/server/src/adapters/fetch.ts @@ -1,12 +1,16 @@ /// +import type { + ContractRouter, +} from '@orpc/contract' import type { PartialOnUndefinedDeep, Promisable, Value, } from '@orpc/shared' -import type { Procedure, WELL_DEFINED_PROCEDURE } from '../procedure' +import type { WELL_DEFINED_PROCEDURE } from '../procedure' import type { Router } from '../router' +import type { RoutingOptions } from '../routing' import { ORPC_HEADER, ORPC_HEADER_VALUE, @@ -24,51 +28,52 @@ import { ORPCSerializer, zodCoerce, } from '@orpc/transformer' -import { isProcedure } from '../procedure' import { createProcedureCaller } from '../procedure-caller' +import { orpcRouting } from '../routing' export interface FetchHandlerHooks { next: () => Promise response: (response: Response) => Response } -export type HandleFetchRequestOptions> = { - /** - * The router used to handle the request. - */ - router: TRouter - - /** - * The request need to be handled. - */ - request: Request - - /** - * Remove the prefix from the request path. - * - * @example /orpc - * @example /api - */ - prefix?: string - - /** - * Hooks for executing logics on lifecycle events. - */ - hooks?: ( - context: TRouter extends Router ? UContext : never, - hooks: FetchHandlerHooks, - ) => Promisable -} & PartialOnUndefinedDeep<{ - /** - * The context used to handle the request. - */ - context: Value< - TRouter extends Router ? UContext : never - > -}> - -export async function handleFetchRequest>( - options: HandleFetchRequestOptions, +export type HandleFetchRequestOptions< + TContractRouter extends ContractRouter | undefined, + TRouter extends Router, +> = + { + /** + * The request need to be handled. + */ + request: Request + + /** + * Remove the prefix from the request path. + * + * @example /orpc + * @example /api + */ + prefix?: string + + /** + * Hooks for executing logics on lifecycle events. + */ + hooks?: ( + context: TRouter extends Router ? UContext : never, + hooks: FetchHandlerHooks, + ) => Promisable + } + & Omit, 'pathname'> + & PartialOnUndefinedDeep<{ + /** + * The context used to handle the request. + */ + context: Value< + TRouter extends Router ? UContext : never + > + }> + +export async function handleFetchRequest>( + options: HandleFetchRequestOptions, ): Promise { const isORPCTransformer = options.request.headers.get(ORPC_HEADER) === ORPC_HEADER_VALUE @@ -89,20 +94,15 @@ export async function handleFetchRequest>( let params: Record | undefined if (isORPCTransformer) { - path = trim(pathname, '/').split('/').map(decodeURIComponent) - - let current: Router | Procedure | undefined = options.router - for (const segment of path) { - if ((typeof current !== 'object' || current === null) && typeof current !== 'function') { - current = undefined - break - } - - current = (current as any)[segment] - } + const match = orpcRouting({ + router: options.router, + pathname, + }) - if (isProcedure(current)) { - procedure = current + if (match) { + procedure = match.procedure + path = match.path + params = match.params } } else { @@ -119,7 +119,7 @@ export async function handleFetchRequest>( schema: procedure.zz$p.contract.zz$cp.InputSchema, }) - const input_ = await (async () => { + const input = await (async () => { try { return await deserializer.deserialize(options.request) } @@ -133,30 +133,11 @@ export async function handleFetchRequest>( } })() - const input = (() => { - if (!params || Object.keys(params).length === 0) { - return input_ - } + const coercedParams = params && procedure.zz$p.contract.zz$cp.InputSchema + ? zodCoerce(procedure.zz$p.contract.zz$cp.InputSchema, params, { bracketNotation: true }) as Record + : params - const coercedParams = procedure.zz$p.contract.zz$cp.InputSchema - ? (zodCoerce( - procedure.zz$p.contract.zz$cp.InputSchema, - { ...params }, - { - bracketNotation: true, - }, - ) as object) - : params - - if (!isPlainObject(input_)) { - return coercedParams - } - - return { - ...coercedParams, - ...input_, - } - })() + const mergedInput = mergeParamsAndInput(coercedParams, input) const caller = createProcedureCaller({ context, @@ -164,7 +145,7 @@ export async function handleFetchRequest>( path, }) - const output = await caller(input) + const output = await caller(mergedInput) const { body, headers } = serializer.serialize(output) @@ -216,3 +197,18 @@ function toORPCError(e: unknown): ORPCError { cause: e, }) } + +function mergeParamsAndInput(coercedParams: Record | undefined, input: unknown) { + if (!coercedParams || Object.keys(coercedParams).length === 0) { + return input + } + + if (!isPlainObject(input)) { + return coercedParams + } + + return { + ...coercedParams, + ...input, + } +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index a3345abf2..4943ef11d 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -9,6 +9,7 @@ export * from './procedure-implementer' export * from './router' export * from './router-caller' export * from './router-implementer' +export * from './routing' export * from './types' export * from './utils' export * from '@orpc/shared/error' diff --git a/packages/server/src/routing.ts b/packages/server/src/routing.ts new file mode 100644 index 000000000..b2dfb2b9e --- /dev/null +++ b/packages/server/src/routing.ts @@ -0,0 +1,57 @@ +import type { ContractRouter } from '@orpc/contract' +import type { Router, RouterWithContract } from './router' +import { trim } from '@orpc/shared' +import { isProcedure, type Procedure } from './procedure' + +export interface RoutingOptions> { + /** + * The `contract router` used for routing the request. + * If not provided, the `router` will be used. + * + * @default undefined + */ + contract?: TContractRouter + + /** + * The `router` used for handling the request, + * and routing if `contract` is not provided. + * + */ + router: TRouter & (TContractRouter extends ContractRouter ? RouterWithContract : unknown) + + /** + * The pathname used for finding the procedure. + */ + pathname: string +} + +export interface Routing { + >( + options: RoutingOptions + ): { + procedure: Procedure + path: string[] + params?: Record + } | undefined +} + +export const orpcRouting: Routing = (options) => { + const path = trim(options.pathname, '/').split('/').map(decodeURIComponent) + + let current: Router | Procedure | undefined = options.router + for (const segment of path) { + if ((typeof current !== 'object' || current === null) && typeof current !== 'function') { + current = undefined + break + } + + current = (current as any)[segment] + } + + return isProcedure(current) + ? { + procedure: current, + path, + } + : undefined +} From f62a0b143b9033d632a06cb1c91e7546bf134f9f Mon Sep 17 00:00:00 2001 From: unnoq Date: Mon, 2 Dec 2024 14:45:02 +0700 Subject: [PATCH 03/11] wip --- packages/server/package.json | 6 +- packages/server/src/adapters/fetch.test.ts | 616 --------------------- packages/server/src/adapters/fetch.ts | 214 ------- packages/server/src/fetch/handle.ts | 32 ++ packages/server/src/fetch/index.ts | 3 + packages/server/src/fetch/orpc-handler.ts | 108 ++++ packages/server/src/fetch/types.ts | 65 +++ packages/server/src/index.ts | 1 - packages/server/src/routing.ts | 57 -- 9 files changed, 211 insertions(+), 891 deletions(-) delete mode 100644 packages/server/src/adapters/fetch.test.ts delete mode 100644 packages/server/src/adapters/fetch.ts create mode 100644 packages/server/src/fetch/handle.ts create mode 100644 packages/server/src/fetch/index.ts create mode 100644 packages/server/src/fetch/orpc-handler.ts create mode 100644 packages/server/src/fetch/types.ts delete mode 100644 packages/server/src/routing.ts diff --git a/packages/server/package.json b/packages/server/package.json index 396835d4a..3f2567a3a 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -21,7 +21,7 @@ "default": "./dist/index.js" }, "./fetch": { - "types": "./dist/src/adapters/fetch.d.ts", + "types": "./dist/src/fetch/index.d.ts", "import": "./dist/fetch.js", "default": "./dist/fetch.js" }, @@ -32,7 +32,7 @@ }, "exports": { ".": "./src/index.ts", - "./fetch": "./src/adapters/fetch.ts", + "./fetch": "./src/fetch/index.ts", "./🔒/*": { "types": "./src/*.ts" } @@ -42,7 +42,7 @@ "dist" ], "scripts": { - "build": "tsup --clean --entry.index=src/index.ts --entry.fetch=src/adapters/fetch.ts --format=esm --onSuccess='tsc -b --noCheck'", + "build": "tsup --clean --entry.index=src/index.ts --entry.fetch=src/fetch/index.ts --format=esm --onSuccess='tsc -b --noCheck'", "build:watch": "pnpm run build --watch", "type:check": "tsc -b" }, diff --git a/packages/server/src/adapters/fetch.test.ts b/packages/server/src/adapters/fetch.test.ts deleted file mode 100644 index 653f3af92..000000000 --- a/packages/server/src/adapters/fetch.test.ts +++ /dev/null @@ -1,616 +0,0 @@ -import { ORPC_HEADER, ORPC_HEADER_VALUE } from '@orpc/contract' -import { oz } from '@orpc/zod' -import { describe, expect, it } from 'vitest' -import { z } from 'zod' -import { ORPCError, os } from '..' -import { handleFetchRequest } from './fetch' - -const router = os.router({ - throw: os.func(() => { - throw new Error('test') - }), - ping: os.func(() => { - return 'ping' - }), - ping2: os.route({ method: 'GET', path: '/ping2' }).func(() => { - return 'ping2' - }), -}) - -describe('simple', () => { - const osw = os.context<{ auth?: boolean }>() - const router = osw.router({ - ping: osw.func(async () => 'pong'), - ping2: osw - .route({ method: 'GET', path: '/ping2' }) - .func(async () => 'pong2'), - }) - - it('200: public url', async () => { - const response = await handleFetchRequest({ - router, - prefix: '/orpc', - request: new Request('http://localhost/orpc/ping', { - method: 'POST', - }), - context: () => ({ auth: true }), - }) - - expect(response.status).toBe(200) - expect(await response.json()).toEqual('pong') - - const response2 = await handleFetchRequest({ - router, - prefix: '/orpc', - request: new Request('http://localhost/orpc/ping2', { - method: 'GET', - }), - context: { auth: true }, - }) - - expect(response2.status).toBe(200) - expect(await response2.json()).toEqual('pong2') - }) - - it('200: internal url', async () => { - const response = await handleFetchRequest({ - router, - request: new Request('http://localhost/ping'), - context: { auth: true }, - }) - - expect(response.status).toBe(200) - expect(await response.json()).toEqual('pong') - - const response2 = await handleFetchRequest({ - router, - prefix: '/orpc', - request: new Request('http://localhost/orpc/ping2'), - context: { auth: true }, - }) - - expect(response2.status).toBe(200) - expect(await response2.json()).toEqual('pong2') - }) - - it('404', async () => { - const response = await handleFetchRequest({ - router, - prefix: '/orpc', - request: new Request('http://localhost/orpc/pingp', { - method: 'POST', - }), - context: { auth: true }, - }) - - expect(response.status).toBe(404) - }) -}) - -describe('procedure throw error', () => { - it('unknown error', async () => { - const response = await handleFetchRequest({ - router, - request: new Request('https://local.com/throw', { method: 'POST' }), - }) - - expect(response.status).toBe(500) - expect(await response.json()).toEqual({ - code: 'INTERNAL_SERVER_ERROR', - status: 500, - message: 'Internal server error', - }) - }) - - it('orpc error', async () => { - const router = os.router({ - ping: os.func(() => { - throw new ORPCError({ code: 'TIMEOUT' }) - }), - }) - - const response = await handleFetchRequest({ - router, - request: new Request('https://local.com/ping', { method: 'POST' }), - }) - - expect(response.status).toBe(408) - expect(await response.json()).toEqual({ - code: 'TIMEOUT', - status: 408, - message: '', - }) - }) - - it('orpc error with data', async () => { - const router = os.router({ - ping: os.func(() => { - throw new ORPCError({ - code: 'PAYLOAD_TOO_LARGE', - message: 'test', - data: { max: '10mb' }, - }) - }), - }) - - const response = await handleFetchRequest({ - router, - request: new Request('https://local.com/ping', { method: 'POST' }), - }) - - expect(response.status).toBe(413) - expect(await response.json()).toEqual({ - code: 'PAYLOAD_TOO_LARGE', - status: 413, - message: 'test', - data: { max: '10mb' }, - }) - }) - - it('orpc error with custom status', async () => { - const router = os.router({ - ping: os.func(() => { - throw new ORPCError({ - code: 'PAYLOAD_TOO_LARGE', - status: 100, - }) - }), - - ping2: os.func(() => { - throw new ORPCError({ - code: 'PAYLOAD_TOO_LARGE', - status: 488, - }) - }), - }) - - const response = await handleFetchRequest({ - router, - request: new Request('https://local.com/ping', { method: 'POST' }), - }) - - expect(response.status).toBe(500) - expect(await response.json()).toEqual({ - code: 'INTERNAL_SERVER_ERROR', - status: 500, - message: 'Internal server error', - }) - - const response2 = await handleFetchRequest({ - router, - request: new Request('https://local.com/ping2', { method: 'POST' }), - }) - - expect(response2.status).toBe(488) - expect(await response2.json()).toEqual({ - code: 'PAYLOAD_TOO_LARGE', - status: 488, - message: '', - }) - }) - - it('input validation error', async () => { - const router = os.router({ - ping: os - .input(z.object({})) - .output(z.string()) - .func(() => { - return 'unnoq' - }), - }) - - const response = await handleFetchRequest({ - router, - request: new Request('https://local.com/ping', { method: 'POST' }), - }) - - expect(response.status).toBe(400) - expect(await response.json()).toEqual({ - code: 'BAD_REQUEST', - status: 400, - message: 'Validation input failed', - issues: [ - { - code: 'invalid_type', - expected: 'object', - message: 'Required', - path: [], - received: 'undefined', - }, - ], - }) - }) - - it('output validation error', async () => { - const router = os.router({ - ping: os - .input(z.string()) - .output(z.string()) - .func(() => { - return 12344 as any - }), - }) - - const response = await handleFetchRequest({ - router, - request: new Request('https://local.com/ping', { - method: 'POST', - body: '"hi"', - }), - }) - - expect(response.status).toBe(500) - expect(await response.json()).toEqual({ - code: 'INTERNAL_SERVER_ERROR', - status: 500, - message: 'Validation output failed', - }) - }) -}) - -describe('hooks', () => { - it('on success', async () => { - const onSuccess = vi.fn() - const onError = vi.fn() - const onFinish = vi.fn() - - await handleFetchRequest({ - prefix: '/orpc', - router, - hooks: async (context, hooks) => { - try { - const response = await hooks.next() - onSuccess(response) - return response - } - catch (e) { - onError(e) - throw e - } - finally { - onFinish() - } - }, - request: new Request('http://localhost/orpc/ping', { - method: 'POST', - }), - }) - - expect(onSuccess).toHaveBeenCalledTimes(1) - expect(onError).toHaveBeenCalledTimes(0) - expect(onFinish).toHaveBeenCalledTimes(1) - - expect(onSuccess.mock.calls[0]?.[0]).toBeInstanceOf(Response) - expect(onFinish.mock.calls[0]?.[1]).toBe(undefined) - }) - - it('on failed', async () => { - const onSuccess = vi.fn() - const onError = vi.fn() - const onFinish = vi.fn() - - await handleFetchRequest({ - router, - hooks: async (context, hooks) => { - try { - const response = await hooks.next() - onSuccess(response) - return response - } - catch (e) { - onError(e) - throw e - } - finally { - onFinish() - } - }, - prefix: '/orpc', - request: new Request('http://localhost/orpc/throw', { - method: 'POST', - }), - }) - - expect(onSuccess).toHaveBeenCalledTimes(0) - expect(onError).toHaveBeenCalledTimes(1) - expect(onFinish).toHaveBeenCalledTimes(1) - - expect(onError.mock.calls[0]?.[0]).toBeInstanceOf(Error) - expect(onError.mock.calls[0]?.[0]?.message).toBe('test') - expect(onFinish.mock.calls[0]?.[0]).toBe(undefined) - }) -}) - -describe('file upload', () => { - const router = os.router({ - signal: os.input(z.instanceof(Blob)).func((input) => { - return input - }), - multiple: os - .input( - z.object({ first: z.instanceof(Blob), second: z.instanceof(Blob) }), - ) - .func((input) => { - return input - }), - }) - - const blob1 = new Blob(['hello'], { type: 'text/plain;charset=utf-8' }) - const blob2 = new Blob(['"world"'], { type: 'image/png' }) - const blob3 = new Blob(['unnoq'], { type: 'application/octet-stream' }) - - it('single file', async () => { - const rForm = new FormData() - rForm.set('meta', JSON.stringify([])) - rForm.set('maps', JSON.stringify([[]])) - rForm.set('0', blob3) - - const response = await handleFetchRequest({ - router, - prefix: '/orpc', - request: new Request('http://localhost/orpc/signal', { - method: 'POST', - body: rForm, - headers: { - [ORPC_HEADER]: ORPC_HEADER_VALUE, - }, - }), - }) - - expect(response.status).toBe(200) - const form = await response.formData() - - const file0 = form.get('0') as File - expect(file0).toBeInstanceOf(File) - expect(file0.name).toBe('blob') - expect(file0.type).toBe('application/octet-stream') - expect(await file0.text()).toBe('unnoq') - }) - - it('multiple file', async () => { - const form = new FormData() - form.set('data', JSON.stringify({ first: blob1, second: blob2 })) - form.set('meta', JSON.stringify([])) - form.set('maps', JSON.stringify([['first'], ['second']])) - form.set('0', blob1) - form.set('1', blob2) - - const response = await handleFetchRequest({ - router, - prefix: '/orpc', - request: new Request('http://localhost/orpc/multiple', { - method: 'POST', - body: form, - headers: { - [ORPC_HEADER]: ORPC_HEADER_VALUE, - }, - }), - }) - - expect(response.status).toBe(200) - - const form_ = await response.formData() - const file0 = form_.get('0') as File - const file1 = form_.get('1') as File - - expect(file0).toBeInstanceOf(File) - expect(file0.name).toBe('blob') - expect(file0.type).toBe('text/plain;charset=utf-8') - expect(await file0.text()).toBe('hello') - - expect(file1).toBeInstanceOf(File) - expect(file1.name).toBe('blob') - expect(file1.type).toBe('image/png') - expect(await file1.text()).toBe('"world"') - }) -}) - -describe('accept header', () => { - const router = os.router({ - ping: os.func(async () => 'pong'), - }) - - it('application/json', async () => { - const response = await handleFetchRequest({ - router, - prefix: '/orpc', - request: new Request('http://localhost/orpc/ping', { - method: 'POST', - headers: { - Accept: 'application/json', - }, - }), - }) - - expect(response.headers.get('Content-Type')).toEqual('application/json') - - expect(await response.json()).toEqual('pong') - }) - - it('multipart/form-data', async () => { - const response = await handleFetchRequest({ - router, - prefix: '/orpc', - request: new Request('http://localhost/orpc/ping', { - method: 'POST', - headers: { - Accept: 'multipart/form-data', - }, - }), - }) - - expect(response.headers.get('Content-Type')).toContain( - 'multipart/form-data', - ) - - const form = await response.formData() - expect(form.get('')).toEqual('pong') - }) - - it('application/x-www-form-urlencoded', async () => { - const response = await handleFetchRequest({ - router, - prefix: '/orpc', - request: new Request('http://localhost/orpc/ping', { - method: 'POST', - headers: { - Accept: 'application/x-www-form-urlencoded', - }, - }), - }) - - expect(response.headers.get('Content-Type')).toEqual( - 'application/x-www-form-urlencoded', - ) - - const params = new URLSearchParams(await response.text()) - expect(params.get('')).toEqual('pong') - }) - - it('*/*', async () => { - const response = await handleFetchRequest({ - router, - prefix: '/orpc', - request: new Request('http://localhost/orpc/ping', { - method: 'POST', - headers: { - Accept: '*/*', - }, - }), - }) - - expect(response.headers.get('Content-Type')).toEqual('application/json') - expect(await response.json()).toEqual('pong') - }) - - it('invalid', async () => { - const response = await handleFetchRequest({ - router, - prefix: '/orpc', - request: new Request('http://localhost/orpc/ping', { - method: 'POST', - headers: { - Accept: 'invalid', - }, - }), - }) - - expect(response.headers.get('Content-Type')).toEqual('application/json') - expect(await response.json()).toEqual({ - code: 'NOT_ACCEPTABLE', - message: 'Unsupported content-type: invalid', - status: 406, - }) - }) -}) - -describe('dynamic params', () => { - const router = os.router({ - deep: os - .route({ - method: 'POST', - path: '/{id}/{id2}', - }) - .input( - z.object({ - id: z.number(), - id2: z.string(), - file: oz.file(), - }), - ) - .func(input => input), - - find: os - .route({ - method: 'GET', - path: '/{id}', - }) - .input( - z.object({ - id: z.number(), - }), - ) - .func(input => input), - }) - - const handlers = [ - { router }, - { router }, // TODO: to test for serverless - ] - - it.each(handlers)('should handle dynamic params', async ({ router }) => { - const response = await handleFetchRequest({ - router, - request: new Request('http://localhost/123'), - }) - - expect(response.status).toEqual(200) - expect(response.headers.get('Content-Type')).toEqual('application/json') - expect(await response.json()).toEqual({ id: 123 }) - }) - - it.each(handlers)('should handle deep dynamic params', async (handler) => { - const form = new FormData() - form.append('file', new Blob(['hello']), 'hello.txt') - - const response = await handleFetchRequest({ - router, - request: new Request('http://localhost/123/dfdsfds', { - method: 'POST', - body: form, - }), - }) - - expect(response.status).toEqual(200) - const rForm = await response.formData() - expect(rForm.get('id')).toEqual('123') - expect(rForm.get('id2')).toEqual('dfdsfds') - }) -}) - -describe('can control method on POST request', () => { - const router = os.router({ - update: os - .route({ - method: 'PUT', - path: '/{id}', - }) - .input( - z.object({ - id: z.number(), - file: oz.file(), - }), - ) - .func(input => input), - }) - - const handlers = [ - { router }, - { router }, // TODO: to test for serverless - ] - - it.each(handlers)('work', async ({ router }) => { - const form = new FormData() - form.set('file', new File(['hello'], 'hello.txt')) - - const response = await handleFetchRequest({ - router, - request: new Request('http://localhost/123', { - method: 'POST', - body: form, - }), - }) - - expect(response.status).toEqual(404) - - const response2 = await handleFetchRequest({ - router, - request: new Request('http://localhost/123?method=PUT', { - method: 'POST', - body: form, - }), - }) - - expect(response2.status).toEqual(200) - }) -}) diff --git a/packages/server/src/adapters/fetch.ts b/packages/server/src/adapters/fetch.ts deleted file mode 100644 index a3424f347..000000000 --- a/packages/server/src/adapters/fetch.ts +++ /dev/null @@ -1,214 +0,0 @@ -/// - -import type { - ContractRouter, -} from '@orpc/contract' -import type { - PartialOnUndefinedDeep, - Promisable, - Value, -} from '@orpc/shared' -import type { WELL_DEFINED_PROCEDURE } from '../procedure' -import type { Router } from '../router' -import type { RoutingOptions } from '../routing' -import { - ORPC_HEADER, - ORPC_HEADER_VALUE, -} from '@orpc/contract' -import { - isPlainObject, - trim, - value, -} from '@orpc/shared' -import { ORPCError } from '@orpc/shared/error' -import { - OpenAPIDeserializer, - OpenAPISerializer, - ORPCDeserializer, - ORPCSerializer, - zodCoerce, -} from '@orpc/transformer' -import { createProcedureCaller } from '../procedure-caller' -import { orpcRouting } from '../routing' - -export interface FetchHandlerHooks { - next: () => Promise - response: (response: Response) => Response -} - -export type HandleFetchRequestOptions< - TContractRouter extends ContractRouter | undefined, - TRouter extends Router, -> = - { - /** - * The request need to be handled. - */ - request: Request - - /** - * Remove the prefix from the request path. - * - * @example /orpc - * @example /api - */ - prefix?: string - - /** - * Hooks for executing logics on lifecycle events. - */ - hooks?: ( - context: TRouter extends Router ? UContext : never, - hooks: FetchHandlerHooks, - ) => Promisable - } - & Omit, 'pathname'> - & PartialOnUndefinedDeep<{ - /** - * The context used to handle the request. - */ - context: Value< - TRouter extends Router ? UContext : never - > - }> - -export async function handleFetchRequest>( - options: HandleFetchRequestOptions, -): Promise { - const isORPCTransformer - = options.request.headers.get(ORPC_HEADER) === ORPC_HEADER_VALUE - const accept = options.request.headers.get('Accept') || undefined - - const serializer = isORPCTransformer - ? new ORPCSerializer() - : new OpenAPISerializer({ accept }) - - const context = await value(options.context) - - const handler = async () => { - const url = new URL(options.request.url) - const pathname = `/${trim(url.pathname.replace(options.prefix ?? '', ''), '/')}` - - let path: string[] | undefined - let procedure: WELL_DEFINED_PROCEDURE | undefined - let params: Record | undefined - - if (isORPCTransformer) { - const match = orpcRouting({ - router: options.router, - pathname, - }) - - if (match) { - procedure = match.procedure - path = match.path - params = match.params - } - } - else { - // TODO - } - - if (!path || !procedure) { - throw new ORPCError({ code: 'NOT_FOUND', message: 'Not found' }) - } - - const deserializer = isORPCTransformer - ? new ORPCDeserializer() - : new OpenAPIDeserializer({ - schema: procedure.zz$p.contract.zz$cp.InputSchema, - }) - - const input = await (async () => { - try { - return await deserializer.deserialize(options.request) - } - catch (e) { - throw new ORPCError({ - code: 'BAD_REQUEST', - message: - 'Cannot parse request. Please check the request body and Content-Type header.', - cause: e, - }) - } - })() - - const coercedParams = params && procedure.zz$p.contract.zz$cp.InputSchema - ? zodCoerce(procedure.zz$p.contract.zz$cp.InputSchema, params, { bracketNotation: true }) as Record - : params - - const mergedInput = mergeParamsAndInput(coercedParams, input) - - const caller = createProcedureCaller({ - context, - procedure, - path, - }) - - const output = await caller(mergedInput) - - const { body, headers } = serializer.serialize(output) - - return new Response(body, { - status: 200, - headers, - }) - } - - try { - return await options.hooks?.(context as any, { - next: handler, - response: response => response, - }) ?? await handler() - } - catch (e) { - const error = toORPCError(e) - - try { - const { body, headers } = serializer.serialize(error.toJSON()) - - return new Response(body, { - status: error.status, - headers, - }) - } - catch (e) { - const error = toORPCError(e) - - // fallback to OpenAPI serializer (without accept) when expected serializer has failed - const { body, headers } = new OpenAPISerializer().serialize( - error.toJSON(), - ) - - return new Response(body, { - status: error.status, - headers, - }) - } - } -} - -function toORPCError(e: unknown): ORPCError { - return e instanceof ORPCError - ? e - : new ORPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: 'Internal server error', - cause: e, - }) -} - -function mergeParamsAndInput(coercedParams: Record | undefined, input: unknown) { - if (!coercedParams || Object.keys(coercedParams).length === 0) { - return input - } - - if (!isPlainObject(input)) { - return coercedParams - } - - return { - ...coercedParams, - ...input, - } -} diff --git a/packages/server/src/fetch/handle.ts b/packages/server/src/fetch/handle.ts new file mode 100644 index 000000000..784006721 --- /dev/null +++ b/packages/server/src/fetch/handle.ts @@ -0,0 +1,32 @@ +import type { ContractRouter } from '@orpc/contract' +import type { Router } from '../router' +import type { FetchHandler, FetchHandlerOptions } from './types' +import { ORPCError } from '@orpc/shared/error' + +export type HandleFetchRequestOptions< + TContractRouter extends ContractRouter | undefined, + TRouter extends Router, +> = FetchHandlerOptions & { + handlers: FetchHandler[] +} + +export async function handleFetchRequest>( + options: HandleFetchRequestOptions, +) { + for (const handler of options.handlers) { + const response = await handler(options) + + if (response) { + return response + } + } + + const error = new ORPCError({ code: 'NOT_FOUND', message: 'Not found' }) + + return new Response(JSON.stringify(error.toJSON()), { + status: error.status, + headers: { + 'Content-Type': 'application/json', + }, + }) +} diff --git a/packages/server/src/fetch/index.ts b/packages/server/src/fetch/index.ts new file mode 100644 index 000000000..31f37a702 --- /dev/null +++ b/packages/server/src/fetch/index.ts @@ -0,0 +1,3 @@ +export * from './handle' +export * from './orpc-handler' +export * from './types' diff --git a/packages/server/src/fetch/orpc-handler.ts b/packages/server/src/fetch/orpc-handler.ts new file mode 100644 index 000000000..0e10ec869 --- /dev/null +++ b/packages/server/src/fetch/orpc-handler.ts @@ -0,0 +1,108 @@ +import type { Procedure } from '../procedure' +import type { Router } from '../router' +import type { FetchHandler } from './types' +import { ORPC_HEADER, ORPC_HEADER_VALUE } from '@orpc/contract' +import { trim, value } from '@orpc/shared' +import { ORPCError } from '@orpc/shared/error' +import { ORPCDeserializer, ORPCSerializer } from '@orpc/transformer' +import { isProcedure } from '../procedure' +import { createProcedureCaller } from '../procedure-caller' + +const serializer = new ORPCSerializer() +const deserializer = new ORPCDeserializer() + +export const ORPCHandler: FetchHandler = async (options) => { + if (options.request.headers.get(ORPC_HEADER) !== ORPC_HEADER_VALUE) { + return undefined + } + + const context = await value(options.context) + + const handler = async () => { + const url = new URL(options.request.url) + const pathname = `/${trim(url.pathname.replace(options.prefix ?? '', ''), '/')}` + + const match = resolveORPCRouter(options.router, pathname) + + if (!match) { + throw new ORPCError({ code: 'NOT_FOUND', message: 'Not found' }) + } + + const input = await deserializeRequest(options.request) + + const caller = createProcedureCaller({ + context, + procedure: match.procedure, + path: match.path, + }) + + const output = await caller(input) + + const { body, headers } = serializer.serialize(output) + + return new Response(body, { + status: 200, + headers, + }) + } + + try { + return await options.hooks?.( + context as any, + { next: handler, response: response => response }, + ) ?? await handler() + } + 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, + }) + } +} + +function resolveORPCRouter(router: Router, pathname: string): { + path: string[] + procedure: Procedure +} | undefined { + const path = trim(pathname, '/').split('/').map(decodeURIComponent) + + let current: Router | Procedure | undefined = router + for (const segment of path) { + if ((typeof current !== 'object' || current === null) && typeof current !== 'function') { + current = undefined + break + } + + current = (current as any)[segment] + } + + return isProcedure(current) + ? { + procedure: current, + path, + } + : undefined +} + +async function deserializeRequest(request: Request): Promise { + try { + return await deserializer.deserialize(request) + } + catch (e) { + throw new ORPCError({ + code: 'BAD_REQUEST', + message: 'Cannot parse request. Please check the request body and Content-Type header.', + cause: e, + }) + } +} diff --git a/packages/server/src/fetch/types.ts b/packages/server/src/fetch/types.ts new file mode 100644 index 000000000..38b0e30bb --- /dev/null +++ b/packages/server/src/fetch/types.ts @@ -0,0 +1,65 @@ +/// + +import type { ContractRouter } from '@orpc/contract' +import type { PartialOnUndefinedDeep, Promisable, Value } from '@orpc/shared' +import type { Router, RouterWithContract } from '../router' + +export interface FetchHandlerHooks { + next: () => Promise + response: (response: Response) => Response +} + +export type FetchHandlerOptions< + TContractRouter extends ContractRouter | undefined, + TRouter extends Router, +> = { + /** + * The `contract router` used for routing the request. + * If not provided, the `router` will be used. + * + * @default undefined + */ + contract?: TContractRouter + + /** + * The `router` used for handling the request, + * and routing if `contract` is not provided. + * + */ + router: TRouter & (TContractRouter extends ContractRouter ? RouterWithContract : unknown) + + /** + * The request need to be handled. + */ + request: Request + + /** + * Remove the prefix from the request path. + * + * @example /orpc + * @example /api + */ + prefix?: string + + /** + * Hooks for executing logics on lifecycle events. + */ + hooks?: ( + context: TRouter extends Router ? UContext : never, + hooks: FetchHandlerHooks, + ) => Promisable +} & PartialOnUndefinedDeep<{ + /** + * The context used to handle the request. + */ + context: Value< + TRouter extends Router ? UContext : never + > +}> + +export type FetchHandler = < + TContractRouter extends ContractRouter | undefined, + TRouter extends Router, +>( + options: FetchHandlerOptions +) => Promise diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 4943ef11d..a3345abf2 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -9,7 +9,6 @@ export * from './procedure-implementer' export * from './router' export * from './router-caller' export * from './router-implementer' -export * from './routing' export * from './types' export * from './utils' export * from '@orpc/shared/error' diff --git a/packages/server/src/routing.ts b/packages/server/src/routing.ts deleted file mode 100644 index b2dfb2b9e..000000000 --- a/packages/server/src/routing.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { ContractRouter } from '@orpc/contract' -import type { Router, RouterWithContract } from './router' -import { trim } from '@orpc/shared' -import { isProcedure, type Procedure } from './procedure' - -export interface RoutingOptions> { - /** - * The `contract router` used for routing the request. - * If not provided, the `router` will be used. - * - * @default undefined - */ - contract?: TContractRouter - - /** - * The `router` used for handling the request, - * and routing if `contract` is not provided. - * - */ - router: TRouter & (TContractRouter extends ContractRouter ? RouterWithContract : unknown) - - /** - * The pathname used for finding the procedure. - */ - pathname: string -} - -export interface Routing { - >( - options: RoutingOptions - ): { - procedure: Procedure - path: string[] - params?: Record - } | undefined -} - -export const orpcRouting: Routing = (options) => { - const path = trim(options.pathname, '/').split('/').map(decodeURIComponent) - - let current: Router | Procedure | undefined = options.router - for (const segment of path) { - if ((typeof current !== 'object' || current === null) && typeof current !== 'function') { - current = undefined - break - } - - current = (current as any)[segment] - } - - return isProcedure(current) - ? { - procedure: current, - path, - } - : undefined -} From 4a328e1362887067f5cc69453567fba32f962b61 Mon Sep 17 00:00:00 2001 From: unnoq Date: Mon, 2 Dec 2024 15:38:54 +0700 Subject: [PATCH 04/11] wip --- packages/openapi/package.json | 9 +- packages/openapi/src/fetch/base-handler.ts | 205 ++++++++++++++++++ packages/openapi/src/fetch/index.ts | 3 + packages/openapi/src/fetch/server-handler.ts | 5 + .../openapi/src/fetch/serverless-handler.ts | 5 + pnpm-lock.yaml | 9 + 6 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 packages/openapi/src/fetch/base-handler.ts create mode 100644 packages/openapi/src/fetch/index.ts create mode 100644 packages/openapi/src/fetch/server-handler.ts create mode 100644 packages/openapi/src/fetch/serverless-handler.ts diff --git a/packages/openapi/package.json b/packages/openapi/package.json index 208061886..337188604 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -20,6 +20,11 @@ "import": "./dist/index.js", "default": "./dist/index.js" }, + "./fetch": { + "types": "./dist/src/fetch/index.d.ts", + "import": "./dist/fetch.js", + "default": "./dist/fetch.js" + }, "./🔒/*": { "types": "./dist/src/*.d.ts" } @@ -27,6 +32,7 @@ }, "exports": { ".": "./src/index.ts", + "./fetch": "./src/fetch/index.ts", "./🔒/*": { "types": "./src/*.ts" } @@ -36,7 +42,7 @@ "dist" ], "scripts": { - "build": "tsup --clean --entry.index=src/index.ts --format=esm --onSuccess='tsc -b --noCheck'", + "build": "tsup --clean --entry.index=src/index.ts --entry.fetch=src/fetch/index.ts --format=esm --onSuccess='tsc -b --noCheck'", "build:watch": "pnpm run build --watch", "type:check": "tsc -b" }, @@ -52,6 +58,7 @@ }, "devDependencies": { "@readme/openapi-parser": "^2.6.0", + "hono": "^4.6.12", "zod": "^3.23.8" } } diff --git a/packages/openapi/src/fetch/base-handler.ts b/packages/openapi/src/fetch/base-handler.ts new file mode 100644 index 000000000..286a55438 --- /dev/null +++ b/packages/openapi/src/fetch/base-handler.ts @@ -0,0 +1,205 @@ +import type { HTTPPath } from '@orpc/contract' +import type { FetchHandler } from '@orpc/server/fetch' +import type { Router as HonoRouter } from 'hono/router' +import { ORPC_HEADER, standardizeHTTPPath } from '@orpc/contract' +import { createProcedureCaller, isProcedure, ORPCError, type Procedure, type Router, type WELL_DEFINED_PROCEDURE } from '@orpc/server' +import { isPlainObject, mapValues, trim, value } from '@orpc/shared' +import { OpenAPIDeserializer, OpenAPISerializer, zodCoerce } from '@orpc/transformer' + +export type ResolveRouter = (router: Router, method: string, pathname: string) => { + path: string[] + procedure: Procedure + params: Record +} | undefined + +type Routing = HonoRouter<[string[], Procedure]> + +export function createOpenAPIHandler(createHonoRouter: () => Routing): FetchHandler { + const resolveRouter = createResolveRouter(createHonoRouter) + + return async (options) => { + if (options.request.headers.get(ORPC_HEADER) !== null) { + return undefined + } + + const context = await value(options.context) + const accept = options.request.headers.get('Accept') || undefined + const serializer = new OpenAPISerializer({ accept }) + + const handler = async () => { + const url = new URL(options.request.url) + const pathname = `/${trim(url.pathname.replace(options.prefix ?? '', ''), '/')}` + + const match = resolveRouter(options.router, options.request.method, pathname) + + if (!match) { + throw new ORPCError({ code: 'NOT_FOUND', message: 'Not found' }) + } + const procedure = match.procedure + const path = match.path + + const params = procedure.zz$p.contract.zz$cp.InputSchema + ? zodCoerce( + procedure.zz$p.contract.zz$cp.InputSchema, + match.params, + { bracketNotation: true }, + ) as Record + : match.params + + const input = await deserializeInput(options.request, procedure) + const mergedInput = mergeParamsAndInput(params, input) + + const caller = createProcedureCaller({ + context, + procedure, + path, + }) + + const output = await caller(mergedInput) + + const { body, headers } = serializer.serialize(output) + + return new Response(body, { + status: 200, + headers, + }) + } + + try { + return await options.hooks?.(context as any, { + next: handler, + response: response => response, + }) ?? await handler() + } + catch (e) { + const error = toORPCError(e) + + try { + const { body, headers } = serializer.serialize(error.toJSON()) + + return new Response(body, { + status: error.status, + headers, + }) + } + catch (e) { + const error = toORPCError(e) + + // fallback to OpenAPI serializer (without accept) when expected serializer has failed + const { body, headers } = new OpenAPISerializer().serialize( + error.toJSON(), + ) + + return new Response(body, { + status: error.status, + headers, + }) + } + } + } +} + +export function createResolveRouter(createHonoRouter: () => Routing): ResolveRouter { + const routingCache = new Map, Routing>() + + return (router: Router, method: string, pathname: string) => { + let routing = routingCache.get(router) + + if (!routing) { + routing = createHonoRouter() + + const addRouteRecursively = (routing: Routing, router: Router, basePath: string[]) => { + for (const key in router) { + const currentPath = [...basePath, key] + const item = router[key] as WELL_DEFINED_PROCEDURE | Router + + if (isProcedure(item)) { + if (item.zz$p.contract.zz$cp.path) { + const method = item.zz$p.contract.zz$cp.method ?? 'POST' + const path = openAPIPathToRouterPath(item.zz$p.contract.zz$cp.path) + + routing.add(method, path, [currentPath, item]) + } + } + else { + addRouteRecursively(routing, item, currentPath) + } + } + } + + addRouteRecursively(routing, router, []) + routingCache.set(router, routing) + } + + const [matches, params_] = routing.match(method, pathname) + + const [match] = matches.sort((a, b) => { + return Object.keys(a[1]).length - Object.keys(b[1]).length + }) + + if (!match) { + return undefined + } + + const path = match[0][0] + const procedure = match[0][1] + const params = params_ + ? mapValues( + (match as any)[1]!, + v => params_[v as number]!, + ) + : match[1] as Record + + return { + path, + procedure, + params: { ...params }, // params from hono not a normal object, so we need spread here + } + } +} + +function mergeParamsAndInput(coercedParams: Record, input: unknown) { + if (Object.keys(coercedParams).length === 0) { + return input + } + + if (!isPlainObject(input)) { + return coercedParams + } + + return { + ...coercedParams, + ...input, + } +} + +async function deserializeInput(request: Request, procedure: Procedure): Promise { + const deserializer = new OpenAPIDeserializer({ + schema: procedure.zz$p.contract.zz$cp.InputSchema, + }) + + try { + return await deserializer.deserialize(request) + } + catch (e) { + throw new ORPCError({ + code: 'BAD_REQUEST', + message: 'Cannot parse request. Please check the request body and Content-Type header.', + cause: e, + }) + } +} + +function toORPCError(e: unknown): ORPCError { + return e instanceof ORPCError + ? e + : new ORPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Internal server error', + cause: e, + }) +} + +export function openAPIPathToRouterPath(path: HTTPPath): string { + return standardizeHTTPPath(path).replace(/\{([^}]+)\}/g, ':$1') +} diff --git a/packages/openapi/src/fetch/index.ts b/packages/openapi/src/fetch/index.ts new file mode 100644 index 000000000..18c20ca54 --- /dev/null +++ b/packages/openapi/src/fetch/index.ts @@ -0,0 +1,3 @@ +export * from './base-handler' +export * from './server-handler' +export * from './serverless-handler' diff --git a/packages/openapi/src/fetch/server-handler.ts b/packages/openapi/src/fetch/server-handler.ts new file mode 100644 index 000000000..32b41fd63 --- /dev/null +++ b/packages/openapi/src/fetch/server-handler.ts @@ -0,0 +1,5 @@ +import type { FetchHandler } from '@orpc/server/fetch' +import { RegExpRouter } from 'hono/router/reg-exp-router' +import { createOpenAPIHandler } from './base-handler' + +export const OpenAPIServerHandler: FetchHandler = createOpenAPIHandler(() => new RegExpRouter()) diff --git a/packages/openapi/src/fetch/serverless-handler.ts b/packages/openapi/src/fetch/serverless-handler.ts new file mode 100644 index 000000000..8d2bd17e7 --- /dev/null +++ b/packages/openapi/src/fetch/serverless-handler.ts @@ -0,0 +1,5 @@ +import type { FetchHandler } from '@orpc/server/fetch' +import { LinearRouter } from 'hono/router/linear-router' +import { createOpenAPIHandler } from './base-handler' + +export const OpenAPIServerlessHandler: FetchHandler = createOpenAPIHandler(() => new LinearRouter()) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3ca992c1d..8b9eccc7f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -200,6 +200,9 @@ importers: '@readme/openapi-parser': specifier: ^2.6.0 version: 2.6.0(openapi-types@12.1.3) + hono: + specifier: ^4.6.12 + version: 4.6.12 zod: specifier: ^3.23.8 version: 3.23.8 @@ -3235,6 +3238,10 @@ packages: hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hono@4.6.12: + resolution: {integrity: sha512-eHtf4kSDNw6VVrdbd5IQi16r22m3s7mWPLd7xOMhg1a/Yyb1A0qpUFq8xYMX4FMuDe1nTKeMX5rTx7Nmw+a+Ag==} + engines: {node: '>=16.9.0'} + hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -8396,6 +8403,8 @@ snapshots: dependencies: '@types/hast': 3.0.4 + hono@4.6.12: {} + hosted-git-info@2.8.9: {} html-encoding-sniffer@4.0.0: From 2459966388c20ac86a1428e85ce3637e587183c4 Mon Sep 17 00:00:00 2001 From: unnoq Date: Mon, 2 Dec 2024 16:08:26 +0700 Subject: [PATCH 05/11] fix types and tests --- apps/content/examples/contract.ts | 23 ++++++--------- apps/content/examples/server.ts | 23 ++++++--------- eslint.config.js | 5 ++++ packages/client/package.json | 1 + packages/client/src/procedure.test.ts | 33 ++++++++++------------ packages/client/src/router.test.ts | 16 +++++------ packages/client/tsconfig.json | 1 + packages/openapi/src/fetch/base-handler.ts | 2 ++ packages/react/tests/orpc.tsx | 8 +++--- packages/server/src/fetch/handle.ts | 10 ++----- packages/server/src/fetch/types.ts | 24 ++++------------ pnpm-lock.yaml | 3 ++ 12 files changed, 64 insertions(+), 85 deletions(-) diff --git a/apps/content/examples/contract.ts b/apps/content/examples/contract.ts index 8f71238a9..bcd20c181 100644 --- a/apps/content/examples/contract.ts +++ b/apps/content/examples/contract.ts @@ -7,15 +7,6 @@ import { z } from 'zod' import { ORPCError, os } from '@orpc/server' -// Expose apis to the internet with fetch handler - -import { createFetchHandler } from '@orpc/server/fetch' - -// Modern runtime that support fetch api like deno, bun, cloudflare workers, even node can used - -import { createServer } from 'node:http' -import { createServerAdapter } from '@whatwg-node/server' - // Define your contract first // This contract can replace server router in most-case @@ -123,20 +114,24 @@ export const router = pub.router({ }, }) -const handler = createFetchHandler({ - router, - serverless: false, // set true will improve cold start times -}) +// Expose apis to the internet with fetch handler +import { OpenAPIServerlessHandler } from '@orpc/openapi/fetch' +import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' +// Modern runtime that support fetch api like deno, bun, cloudflare workers, even node can used +import { createServer } from 'node:http' +import { createServerAdapter } from '@whatwg-node/server' const server = createServer( createServerAdapter((request: Request) => { const url = new URL(request.url) if (url.pathname.startsWith('/api')) { - return handler({ + return handleFetchRequest({ + router, request, prefix: '/api', context: {}, + handlers: [ORPCHandler, OpenAPIServerlessHandler], }) } diff --git a/apps/content/examples/server.ts b/apps/content/examples/server.ts index 5514e872b..9db135ff0 100644 --- a/apps/content/examples/server.ts +++ b/apps/content/examples/server.ts @@ -3,15 +3,6 @@ import { ORPCError, os } from '@orpc/server' import { oz } from '@orpc/zod' import { z } from 'zod' -// Expose apis to the internet with fetch handler - -import { createFetchHandler } from '@orpc/server/fetch' - -// Modern runtime that support fetch api like deno, bun, cloudflare workers, even node can used - -import { createServer } from 'node:http' -import { createServerAdapter } from '@whatwg-node/server' - export type Context = { user?: { id: string } } // global pub, authed completely optional @@ -97,20 +88,24 @@ export const router = pub.router({ export type Inputs = InferRouterInputs export type Outputs = InferRouterOutputs -const handler = createFetchHandler({ - router, - serverless: false, // set true will improve cold start times -}) +// Expose apis to the internet with fetch handler +import { OpenAPIServerlessHandler } from '@orpc/openapi/fetch' +import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' +// Modern runtime that support fetch api like deno, bun, cloudflare workers, even node can used +import { createServer } from 'node:http' +import { createServerAdapter } from '@whatwg-node/server' const server = createServer( createServerAdapter((request: Request) => { const url = new URL(request.url) if (url.pathname.startsWith('/api')) { - return handler({ + return handleFetchRequest({ + router, request, prefix: '/api', context: {}, + handlers: [ORPCHandler, OpenAPIServerlessHandler], }) } diff --git a/eslint.config.js b/eslint.config.js index fcde1927e..424b25363 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -15,6 +15,11 @@ export default antfu({ 'unused-imports/no-unused-vars': 'off', 'antfu/no-top-level-await': 'off', }, +}, { + files: ['apps/content/examples/**'], + rules: { + 'import/first': 'off', + }, }, { files: ['playgrounds/**'], rules: { diff --git a/packages/client/package.json b/packages/client/package.json index ec28e1175..c015d87b3 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -49,6 +49,7 @@ "@orpc/transformer": "workspace:*" }, "devDependencies": { + "@orpc/openapi": "workspace:*", "zod": "^3.23.8" } } diff --git a/packages/client/src/procedure.test.ts b/packages/client/src/procedure.test.ts index af000215f..3b82d33e1 100644 --- a/packages/client/src/procedure.test.ts +++ b/packages/client/src/procedure.test.ts @@ -1,4 +1,6 @@ +import { OpenAPIServerHandler, OpenAPIServerlessHandler } from '@orpc/openapi/fetch' import { ORPCError, os } from '@orpc/server' +import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' import { z } from 'zod' import { createProcedureClient } from './procedure' @@ -13,12 +15,12 @@ describe('createProcedureClient', () => { ping, }, }) - const handler = handleFetchRequest({ - router, - }) + const orpcFetch: typeof fetch = async (...args) => { const request = new Request(...args) - const response = await handler({ + const response = await handleFetchRequest({ + router, + handlers: [OpenAPIServerHandler, OpenAPIServerlessHandler, ORPCHandler], // make sure still work with openapi handlers prefix: '/orpc', request, context: {}, @@ -113,16 +115,14 @@ describe('createProcedureClient', () => { .func(input => input.value), }) - const handler = handleFetchRequest({ - router, - }) - const client = createProcedureClient({ path: ['ping'], baseURL: 'http://localhost:3000/orpc', fetch: (...args) => { const request = new Request(...args) - return handler({ + return handleFetchRequest({ + router, + handlers: [ORPCHandler, OpenAPIServerHandler], prefix: '/orpc', request, context: {}, @@ -146,16 +146,14 @@ describe('createProcedureClient', () => { }), }) - const handler = handleFetchRequest({ - router, - }) - const client = createProcedureClient({ path: ['ping'], baseURL: 'http://localhost:3000/orpc', fetch: (...args) => { const request = new Request(...args) - return handler({ + return handleFetchRequest({ + router, + handlers: [ORPCHandler], prefix: '/orpc', request, context: {}, @@ -191,16 +189,15 @@ describe('upload file', () => { }), }) - const handler = handleFetchRequest({ router }) - const orpcFetch: typeof fetch = async (...args) => { const request = new Request(...args) - const response = await handler({ + return handleFetchRequest({ + router, + handlers: [ORPCHandler], prefix: '/orpc', request, context: {}, }) - return response } const blob1 = new Blob(['hello'], { type: 'text/plain;charset=utf-8' }) diff --git a/packages/client/src/router.test.ts b/packages/client/src/router.test.ts index f79ceb153..882a91ba9 100644 --- a/packages/client/src/router.test.ts +++ b/packages/client/src/router.test.ts @@ -1,5 +1,6 @@ import { oc } from '@orpc/contract' import { os } from '@orpc/server' +import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' import { z } from 'zod' import { createRouterClient } from './router' @@ -14,15 +15,14 @@ describe('createRouterClient', () => { unique: ping, }, }) - const handler = handleFetchRequest({ - router, - }) const orpcFetch: typeof fetch = async (...args) => { const request = new Request(...args) - return await handler({ + return await handleFetchRequest({ + router, prefix: '/orpc', request, context: {}, + handlers: [ORPCHandler], }) } @@ -125,18 +125,16 @@ describe('createRouterClient', () => { .func(input => input.value), }) - const handler = handleFetchRequest({ - router, - }) - const client = createRouterClient({ baseURL: 'http://localhost:3000/orpc', fetch: (...args) => { const request = new Request(...args) - return handler({ + return handleFetchRequest({ + router, prefix: '/orpc', request, context: {}, + handlers: [ORPCHandler], }) }, }) diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index 0b79da693..43168db65 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -5,6 +5,7 @@ }, "references": [ { "path": "../contract" }, + { "path": "../openapi" }, { "path": "../server" }, { "path": "../shared" }, { "path": "../transformer" } diff --git a/packages/openapi/src/fetch/base-handler.ts b/packages/openapi/src/fetch/base-handler.ts index 286a55438..ef10c0f58 100644 --- a/packages/openapi/src/fetch/base-handler.ts +++ b/packages/openapi/src/fetch/base-handler.ts @@ -1,3 +1,5 @@ +/// + import type { HTTPPath } from '@orpc/contract' import type { FetchHandler } from '@orpc/server/fetch' import type { Router as HonoRouter } from 'hono/router' diff --git a/packages/react/tests/orpc.tsx b/packages/react/tests/orpc.tsx index 327679dd2..860a27d38 100644 --- a/packages/react/tests/orpc.tsx +++ b/packages/react/tests/orpc.tsx @@ -1,5 +1,6 @@ import { createORPCClient } from '@orpc/client' import { os } from '@orpc/server' +import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import React, { Suspense } from 'react' import { z } from 'zod' @@ -91,8 +92,6 @@ export const appRouter = orpcServer.router({ }, }) -export const appHandler = handleFetchRequest({ router: appRouter }) - export const orpcClient = createORPCClient({ baseURL: 'http://localhost:3000', @@ -100,9 +99,10 @@ export const orpcClient = createORPCClient({ await new Promise(resolve => setTimeout(resolve, 100)) const request = new Request(...args) - return appHandler({ + return handleFetchRequest({ + router: appRouter, request, - context: undefined, + handlers: [ORPCHandler], }) }, }) diff --git a/packages/server/src/fetch/handle.ts b/packages/server/src/fetch/handle.ts index 784006721..a45e76bc7 100644 --- a/packages/server/src/fetch/handle.ts +++ b/packages/server/src/fetch/handle.ts @@ -1,17 +1,13 @@ -import type { ContractRouter } from '@orpc/contract' import type { Router } from '../router' import type { FetchHandler, FetchHandlerOptions } from './types' import { ORPCError } from '@orpc/shared/error' -export type HandleFetchRequestOptions< - TContractRouter extends ContractRouter | undefined, - TRouter extends Router, -> = FetchHandlerOptions & { +export type HandleFetchRequestOptions> = FetchHandlerOptions & { handlers: FetchHandler[] } -export async function handleFetchRequest>( - options: HandleFetchRequestOptions, +export async function handleFetchRequest< TRouter extends Router>( + options: HandleFetchRequestOptions, ) { for (const handler of options.handlers) { const response = await handler(options) diff --git a/packages/server/src/fetch/types.ts b/packages/server/src/fetch/types.ts index 38b0e30bb..0e4237f05 100644 --- a/packages/server/src/fetch/types.ts +++ b/packages/server/src/fetch/types.ts @@ -1,8 +1,7 @@ /// -import type { ContractRouter } from '@orpc/contract' import type { PartialOnUndefinedDeep, Promisable, Value } from '@orpc/shared' -import type { Router, RouterWithContract } from '../router' +import type { Router } from '../router' export interface FetchHandlerHooks { next: () => Promise @@ -10,23 +9,13 @@ export interface FetchHandlerHooks { } export type FetchHandlerOptions< - TContractRouter extends ContractRouter | undefined, TRouter extends Router, > = { /** - * The `contract router` used for routing the request. - * If not provided, the `router` will be used. + * The `router` used for handling the request and routing, * - * @default undefined */ - contract?: TContractRouter - - /** - * The `router` used for handling the request, - * and routing if `contract` is not provided. - * - */ - router: TRouter & (TContractRouter extends ContractRouter ? RouterWithContract : unknown) + router: TRouter /** * The request need to be handled. @@ -57,9 +46,6 @@ export type FetchHandlerOptions< > }> -export type FetchHandler = < - TContractRouter extends ContractRouter | undefined, - TRouter extends Router, ->( - options: FetchHandlerOptions +export type FetchHandler = >( + options: FetchHandlerOptions ) => Promise diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b9eccc7f..fefcb81ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -157,6 +157,9 @@ importers: specifier: workspace:* version: link:../transformer devDependencies: + '@orpc/openapi': + specifier: workspace:* + version: link:../openapi zod: specifier: ^3.23.8 version: 3.23.8 From 89ddd0620cdd784aeb9174e0438c0b92f8bebc9d Mon Sep 17 00:00:00 2001 From: unnoq Date: Mon, 2 Dec 2024 16:34:30 +0700 Subject: [PATCH 06/11] docs fixed --- apps/content/content/docs/index.mdx | 12 ++- apps/content/content/docs/server/context.mdx | 22 +++-- .../content/docs/server/integrations.mdx | 83 ++++++++----------- playgrounds/contract-openapi/src/main.ts | 29 ++++--- playgrounds/expressjs/src/main.ts | 29 ++++--- .../nextjs/src/app/api/[...rest]/route.ts | 26 +++--- playgrounds/openapi/src/main.ts | 29 ++++--- 7 files changed, 102 insertions(+), 128 deletions(-) diff --git a/apps/content/content/docs/index.mdx b/apps/content/content/docs/index.mdx index 87b5ef830..57b9e7aec 100644 --- a/apps/content/content/docs/index.mdx +++ b/apps/content/content/docs/index.mdx @@ -144,25 +144,23 @@ In oRPC middleware is very useful and fully typed you can find more info [here]( This example uses [@whatwg-node/server](https://www.npmjs.com/package/@whatwg-node/server) to create a Node server with [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). ```ts twoslash -import { createFetchHandler } from '@orpc/server/fetch' +import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' +import { OpenAPIServerlessHandler } from '@orpc/openapi/fetch' import { createServer } from 'node:http' import { createServerAdapter } from '@whatwg-node/server' import { router } from 'examples/server' -const handler = createFetchHandler({ - router, - serverless: false, // set true will improve cold start times -}) - const server = createServer( createServerAdapter((request: Request) => { const url = new URL(request.url) if (url.pathname.startsWith('/api')) { - return handler({ + return handleFetchRequest({ + router, request, prefix: '/api', context: {}, + handlers: [ORPCHandler, OpenAPIServerlessHandler], }) } diff --git a/apps/content/content/docs/server/context.mdx b/apps/content/content/docs/server/context.mdx index e2a15ab10..d92eafaba 100644 --- a/apps/content/content/docs/server/context.mdx +++ b/apps/content/content/docs/server/context.mdx @@ -46,7 +46,8 @@ If your procedure only depends on `Middleware Context`, you can ```ts twoslash import { os, ORPCError } from '@orpc/server' -import { createFetchHandler } from '@orpc/server/fetch' +import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' +import { OpenAPIServerlessHandler, OpenAPIServerHandler } from '@orpc/openapi/fetch' import { headers } from 'next/headers' const base = os.use(async (input, context, meta) => { @@ -83,14 +84,12 @@ export const router = base.router({ // You can call this procedure directly without manually providing context const output = await router.getting() -const handler = createFetchHandler({ - router, -}) - export function fetch(request: Request) { // No need to pass context; middleware handles it - return handler({ + return handleFetchRequest({ + router, request, + handlers: [ORPCHandler, OpenAPIServerlessHandler], }) } ``` @@ -103,7 +102,8 @@ rather than relying on global mechanisms like `headers` or `cookies` in Next.js. ```ts twoslash import { os, ORPCError, createProcedureCaller } from '@orpc/server' -import { createFetchHandler } from '@orpc/server/fetch' +import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' +import { OpenAPIServerlessHandler, OpenAPIServerHandler } from '@orpc/openapi/fetch' type ORPCContext = { user?: { id: string }, db: 'fake-db' } @@ -129,18 +129,16 @@ export const router = base.router({ }), }) -const handler = createFetchHandler({ - router, -}) - export function fetch(request: Request) { // Initialize context explicitly for each request const db = 'fake-db' as const const user = request.headers.get('Authorization') ? { id: 'example' } : undefined - return handler({ + return handleFetchRequest({ + router, request, context: { db, user }, + handlers: [ORPCHandler, OpenAPIServerlessHandler], }) } diff --git a/apps/content/content/docs/server/integrations.mdx b/apps/content/content/docs/server/integrations.mdx index 6469394fd..2eed8f541 100644 --- a/apps/content/content/docs/server/integrations.mdx +++ b/apps/content/content/docs/server/integrations.mdx @@ -13,19 +13,17 @@ Whether you're targeting serverless, edge environments, or traditional backends, ## Quick Example ```ts twoslash -import { createFetchHandler } from '@orpc/server/fetch' +import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' +import { OpenAPIServerlessHandler, OpenAPIServerHandler } from '@orpc/openapi/fetch' import { router } from 'examples/server' -const handler = createFetchHandler({ - router, - serverless: false, // Set true for faster cold starts -}) - export function fetch(request: Request) { - return handler({ + return handleFetchRequest({ + router, request, context: {}, // prefix: '/api', // Optionally define a route prefix + handlers: [ORPCHandler, OpenAPIServerlessHandler], }) } ``` @@ -36,22 +34,20 @@ Node.js doesn't provide native support for creating server with [Fetch API](http but you can easily use [@whatwg-node/server](https://npmjs.com/package/@whatwg-node/server) as an adapter. ```ts twoslash -import { createFetchHandler } from '@orpc/server/fetch' +import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' +import { OpenAPIServerlessHandler, OpenAPIServerHandler } from '@orpc/openapi/fetch' import { createServer } from 'node:http' import { createServerAdapter } from '@whatwg-node/server' import { router } from 'examples/server' -const handler = createFetchHandler({ - router, - serverless: false, -}) - const server = createServer( createServerAdapter((request: Request) => { - return handler({ + return handleFetchRequest({ + router, request, context: {}, // prefix: '/api', + handlers: [ORPCHandler, OpenAPIServerHandler], }) }) ) @@ -64,23 +60,21 @@ server.listen(3000, () => { ## Express.js ```ts twoslash -import { createFetchHandler } from '@orpc/server/fetch' +import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' +import { OpenAPIServerlessHandler, OpenAPIServerHandler } from '@orpc/openapi/fetch' import { createServerAdapter } from '@whatwg-node/server' import express from 'express' import { router } from 'examples/server' -const handler = createFetchHandler({ - router, - serverless: false, -}) - const app = express() app.all('/api/*', createServerAdapter((request: Request) => { - return handler({ + return handleFetchRequest({ + router, request, context: {}, prefix: '/api', + handlers: [ORPCHandler, OpenAPIServerHandler], }) })) @@ -93,21 +87,19 @@ app.listen(3000, () => { ```ts twoslash import { Hono } from 'hono' -import { createFetchHandler } from '@orpc/server/fetch' +import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' +import { OpenAPIServerlessHandler, OpenAPIServerHandler } from '@orpc/openapi/fetch' import { router } from 'examples/server' -const handler = createFetchHandler({ - router, - serverless: false, -}) - const app = new Hono() app.get('/api/*', (c) => { - return handler({ + return handleFetchRequest({ + router, request: c.req.raw, prefix: '/api', context: {}, + handlers: [ORPCHandler, OpenAPIServerlessHandler], }) }) @@ -117,46 +109,41 @@ export default app ## Next.js ```ts title="app/api/[...orpc]/route.ts" twoslash -import { createFetchHandler } from '@orpc/server/fetch' +import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' +import { OpenAPIServerlessHandler, OpenAPIServerHandler } from '@orpc/openapi/fetch' import { router } from 'examples/server' -const handler = createFetchHandler({ - router, - serverless: true, -}) - -const fetchRequestHandler = async (request: Request) => { - return handler({ +export function GET(request: Request) { + return handleFetchRequest({ + router, request, prefix: '/api', context: {}, + handlers: [ORPCHandler, OpenAPIServerlessHandler], }) } -export const GET = fetchRequestHandler -export const POST = fetchRequestHandler -export const PUT = fetchRequestHandler -export const DELETE = fetchRequestHandler -export const PATCH = fetchRequestHandler +export const POST = GET +export const PUT = GET +export const DELETE = GET +export const PATCH = GET ``` ## Cloudflare Workers ```ts twoslash -import { createFetchHandler } from '@orpc/server/fetch' +import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' +import { OpenAPIServerlessHandler, OpenAPIServerHandler } from '@orpc/openapi/fetch' import { router } from 'examples/server' -const handler = createFetchHandler({ - router, - serverless: true, -}) - export default { async fetch(request: Request) { - return handler({ + return handleFetchRequest({ + router, request, prefix: '/', context: {}, + handlers: [ORPCHandler, OpenAPIServerlessHandler], }) }, } diff --git a/playgrounds/contract-openapi/src/main.ts b/playgrounds/contract-openapi/src/main.ts index a64577085..54a493061 100644 --- a/playgrounds/contract-openapi/src/main.ts +++ b/playgrounds/contract-openapi/src/main.ts @@ -1,24 +1,12 @@ import { createServer } from 'node:http' import { generateOpenAPI } from '@orpc/openapi' -import { createFetchHandler } from '@orpc/server/fetch' +import { OpenAPIServerHandler } from '@orpc/openapi/fetch' +import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' import { createServerAdapter } from '@whatwg-node/server' import { contract } from './contract' import { router } from './router' import './polyfill' -const orpcHandler = createFetchHandler({ - router, - async hooks(context, hooks) { - try { - return hooks.next() - } - catch (e) { - console.error(e) - throw e - } - }, -}) - const server = createServer( createServerAdapter((request: Request) => { const url = new URL(request.url) @@ -28,10 +16,21 @@ const server = createServer( : {} if (url.pathname.startsWith('/api')) { - return orpcHandler({ + return handleFetchRequest({ + router, request, prefix: '/api', context, + handlers: [ORPCHandler, OpenAPIServerHandler], + async hooks(context, hooks) { + try { + return hooks.next() + } + catch (e) { + console.error(e) + throw e + } + }, }) } diff --git a/playgrounds/expressjs/src/main.ts b/playgrounds/expressjs/src/main.ts index 4b663adaf..595e5367b 100644 --- a/playgrounds/expressjs/src/main.ts +++ b/playgrounds/expressjs/src/main.ts @@ -1,5 +1,6 @@ import { generateOpenAPI } from '@orpc/openapi' -import { createFetchHandler } from '@orpc/server/fetch' +import { OpenAPIServerHandler } from '@orpc/openapi/fetch' +import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' import { createServerAdapter } from '@whatwg-node/server' import express from 'express' import { router } from './router' @@ -7,19 +8,6 @@ import './polyfill' const app = express() -const orpcHandler = createFetchHandler({ - router, - async hooks(context, hooks) { - try { - return hooks.next() - } - catch (e) { - console.error(e) - throw e - } - }, -}) - app.all( '/api/*', createServerAdapter((request: Request) => { @@ -27,10 +15,21 @@ app.all( ? { user: { id: 'test', name: 'John Doe', email: 'john@doe.com' } } : {} - return orpcHandler({ + return handleFetchRequest({ request, prefix: '/api', context, + router, + handlers: [ORPCHandler, OpenAPIServerHandler], + async hooks(context, hooks) { + try { + return hooks.next() + } + catch (e) { + console.error(e) + throw e + } + }, }) }), ) diff --git a/playgrounds/nextjs/src/app/api/[...rest]/route.ts b/playgrounds/nextjs/src/app/api/[...rest]/route.ts index 2191b851c..eeacc4110 100644 --- a/playgrounds/nextjs/src/app/api/[...rest]/route.ts +++ b/playgrounds/nextjs/src/app/api/[...rest]/route.ts @@ -1,23 +1,17 @@ -import { createFetchHandler } from '@orpc/server/fetch' +import { OpenAPIServerHandler } from '@orpc/openapi/fetch' +import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' import { router } from './router' -const handler = createFetchHandler({ - router, - serverless: true, - // hooks(context, hooks) { - // return hooks.next() - // }, -}) - -function handleRequest(request: Request) { - return handler({ +export function GET(request: Request) { + return handleFetchRequest({ + router, request, prefix: '/api', + handlers: [ORPCHandler, OpenAPIServerHandler], }) } -export const GET = handleRequest -export const POST = handleRequest -export const PUT = handleRequest -export const DELETE = handleRequest -export const PATCH = handleRequest +export const POST = GET +export const PUT = GET +export const DELETE = GET +export const PATCH = GET diff --git a/playgrounds/openapi/src/main.ts b/playgrounds/openapi/src/main.ts index 00e0119ce..b485d05f7 100644 --- a/playgrounds/openapi/src/main.ts +++ b/playgrounds/openapi/src/main.ts @@ -1,23 +1,11 @@ import { createServer } from 'node:http' import { generateOpenAPI } from '@orpc/openapi' -import { createFetchHandler } from '@orpc/server/fetch' +import { OpenAPIServerHandler } from '@orpc/openapi/fetch' +import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' import { createServerAdapter } from '@whatwg-node/server' import { router } from './router' import './polyfill' -const orpcHandler = createFetchHandler({ - router, - async hooks(context, hooks) { - try { - return hooks.next() - } - catch (e) { - console.error(e) - throw e - } - }, -}) - const server = createServer( createServerAdapter((request: Request) => { const url = new URL(request.url) @@ -27,10 +15,21 @@ const server = createServer( : {} if (url.pathname.startsWith('/api')) { - return orpcHandler({ + return handleFetchRequest({ request, prefix: '/api', context, + router, + handlers: [ORPCHandler, OpenAPIServerHandler], + async hooks(context, hooks) { + try { + return hooks.next() + } + catch (e) { + console.error(e) + throw e + } + }, }) } From e3aa2b9eaf4d0e1667c70e32bfacda6afa6b3254 Mon Sep 17 00:00:00 2001 From: unnoq Date: Mon, 2 Dec 2024 16:35:43 +0700 Subject: [PATCH 07/11] require at least one handler --- packages/server/src/fetch/handle.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/fetch/handle.ts b/packages/server/src/fetch/handle.ts index a45e76bc7..80e5bd3be 100644 --- a/packages/server/src/fetch/handle.ts +++ b/packages/server/src/fetch/handle.ts @@ -3,7 +3,7 @@ import type { FetchHandler, FetchHandlerOptions } from './types' import { ORPCError } from '@orpc/shared/error' export type HandleFetchRequestOptions> = FetchHandlerOptions & { - handlers: FetchHandler[] + handlers: [FetchHandler, ...FetchHandler[]] } export async function handleFetchRequest< TRouter extends Router>( From 1634a904a2ee798807d4449f6b8b838c39a09ec3 Mon Sep 17 00:00:00 2001 From: unnoq Date: Mon, 2 Dec 2024 16:51:57 +0700 Subject: [PATCH 08/11] tests --- packages/openapi/src/fetch/base-handler.ts | 17 +- packages/server/package.json | 3 + packages/server/src/fetch/handle.test.ts | 651 +++++++++++++++++++++ packages/server/src/fetch/handle.ts | 2 +- pnpm-lock.yaml | 4 + 5 files changed, 670 insertions(+), 7 deletions(-) create mode 100644 packages/server/src/fetch/handle.test.ts diff --git a/packages/openapi/src/fetch/base-handler.ts b/packages/openapi/src/fetch/base-handler.ts index ef10c0f58..e98fa6efa 100644 --- a/packages/openapi/src/fetch/base-handler.ts +++ b/packages/openapi/src/fetch/base-handler.ts @@ -31,8 +31,13 @@ export function createOpenAPIHandler(createHonoRouter: () => Routing): FetchHand const handler = async () => { const url = new URL(options.request.url) const pathname = `/${trim(url.pathname.replace(options.prefix ?? '', ''), '/')}` + const customMethod + = options.request.method === 'POST' + ? url.searchParams.get('method')?.toUpperCase() + : undefined + const method = customMethod || options.request.method - const match = resolveRouter(options.router, options.request.method, pathname) + const match = resolveRouter(options.router, method, pathname) if (!match) { throw new ORPCError({ code: 'NOT_FOUND', message: 'Not found' }) @@ -116,12 +121,12 @@ export function createResolveRouter(createHonoRouter: () => Routing): ResolveRou const item = router[key] as WELL_DEFINED_PROCEDURE | Router if (isProcedure(item)) { - if (item.zz$p.contract.zz$cp.path) { - const method = item.zz$p.contract.zz$cp.method ?? 'POST' - const path = openAPIPathToRouterPath(item.zz$p.contract.zz$cp.path) + const method = item.zz$p.contract.zz$cp.method ?? 'POST' + const path = item.zz$p.contract.zz$cp.path + ? openAPIPathToRouterPath(item.zz$p.contract.zz$cp.path) + : `/${currentPath.map(encodeURIComponent).join('/')}` - routing.add(method, path, [currentPath, item]) - } + routing.add(method, path, [currentPath, item]) } else { addRouteRecursively(routing, item, currentPath) diff --git a/packages/server/package.json b/packages/server/package.json index 3f2567a3a..6091160bc 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -54,5 +54,8 @@ "@orpc/contract": "workspace:*", "@orpc/shared": "workspace:*", "@orpc/transformer": "workspace:*" + }, + "devDependencies": { + "@orpc/openapi": "workspace:*" } } diff --git a/packages/server/src/fetch/handle.test.ts b/packages/server/src/fetch/handle.test.ts new file mode 100644 index 000000000..a01d3a568 --- /dev/null +++ b/packages/server/src/fetch/handle.test.ts @@ -0,0 +1,651 @@ +import { ORPC_HEADER, ORPC_HEADER_VALUE } from '@orpc/contract' +import { OpenAPIServerHandler, OpenAPIServerlessHandler } from '@orpc/openapi/fetch' +import { oz } from '@orpc/zod' +import { describe, expect, it } from 'vitest' +import { z } from 'zod' +import { ORPCError, os } from '..' +import { handleFetchRequest } from './handle' +import { ORPCHandler } from './orpc-handler' + +const router = os.router({ + throw: os.func(() => { + throw new Error('test') + }), + ping: os.func(() => { + return 'ping' + }), + ping2: os.route({ method: 'GET', path: '/ping2' }).func(() => { + return 'ping2' + }), +}) + +describe('simple', () => { + const osw = os.context<{ auth?: boolean }>() + const router = osw.router({ + ping: osw.func(async () => 'pong'), + ping2: osw + .route({ method: 'GET', path: '/ping2' }) + .func(async () => 'pong2'), + }) + + it('200: public url', async () => { + const response = await handleFetchRequest({ + router, + handlers: [ORPCHandler, OpenAPIServerlessHandler], + prefix: '/orpc', + request: new Request('http://localhost/orpc/ping', { + method: 'POST', + }), + context: () => ({ auth: true }), + }) + + expect(response.status).toBe(200) + expect(await response.json()).toEqual('pong') + + const response2 = await handleFetchRequest({ + router, + handlers: [ORPCHandler, OpenAPIServerlessHandler], + prefix: '/orpc', + request: new Request('http://localhost/orpc/ping2', { + method: 'GET', + }), + context: { auth: true }, + }) + + expect(response2.status).toBe(200) + expect(await response2.json()).toEqual('pong2') + }) + + it('200: internal url', async () => { + const response = await handleFetchRequest({ + router, + handlers: [ORPCHandler, OpenAPIServerlessHandler], + request: new Request('http://localhost/ping', { + method: 'POST', + }), + context: { auth: true }, + }) + + expect(response.status).toBe(200) + expect(await response.json()).toEqual('pong') + + const response2 = await handleFetchRequest({ + router, + handlers: [ORPCHandler, OpenAPIServerlessHandler], + prefix: '/orpc', + request: new Request('http://localhost/orpc/ping2'), + context: { auth: true }, + }) + + expect(response2.status).toBe(200) + expect(await response2.json()).toEqual('pong2') + }) + + it('404', async () => { + const response = await handleFetchRequest({ + router, + handlers: [ORPCHandler, OpenAPIServerHandler], + prefix: '/orpc', + request: new Request('http://localhost/orpc/pingp', { + method: 'POST', + }), + context: { auth: true }, + }) + + expect(response.status).toBe(404) + }) +}) + +describe('procedure throw error', () => { + it('unknown error', async () => { + const response = await handleFetchRequest({ + router, + handlers: [ORPCHandler, OpenAPIServerHandler], + request: new Request('https://local.com/throw', { method: 'POST' }), + }) + + expect(response.status).toBe(500) + expect(await response.json()).toEqual({ + code: 'INTERNAL_SERVER_ERROR', + status: 500, + message: 'Internal server error', + }) + }) + + it('orpc error', async () => { + const router = os.router({ + ping: os.func(() => { + throw new ORPCError({ code: 'TIMEOUT' }) + }), + }) + + const response = await handleFetchRequest({ + router, + handlers: [ORPCHandler, OpenAPIServerHandler], + request: new Request('https://local.com/ping', { method: 'POST' }), + }) + + expect(response.status).toBe(408) + expect(await response.json()).toEqual({ + code: 'TIMEOUT', + status: 408, + message: '', + }) + }) + + it('orpc error with data', async () => { + const router = os.router({ + ping: os.func(() => { + throw new ORPCError({ + code: 'PAYLOAD_TOO_LARGE', + message: 'test', + data: { max: '10mb' }, + }) + }), + }) + + const response = await handleFetchRequest({ + router, + handlers: [ORPCHandler, OpenAPIServerHandler], + request: new Request('https://local.com/ping', { method: 'POST' }), + }) + + expect(response.status).toBe(413) + expect(await response.json()).toEqual({ + code: 'PAYLOAD_TOO_LARGE', + status: 413, + message: 'test', + data: { max: '10mb' }, + }) + }) + + it('orpc error with custom status', async () => { + const router = os.router({ + ping: os.func(() => { + throw new ORPCError({ + code: 'PAYLOAD_TOO_LARGE', + status: 100, + }) + }), + + ping2: os.func(() => { + throw new ORPCError({ + code: 'PAYLOAD_TOO_LARGE', + status: 488, + }) + }), + }) + + const response = await handleFetchRequest({ + router, + handlers: [ORPCHandler, OpenAPIServerHandler], + request: new Request('https://local.com/ping', { method: 'POST' }), + }) + + expect(response.status).toBe(500) + expect(await response.json()).toEqual({ + code: 'INTERNAL_SERVER_ERROR', + status: 500, + message: 'Internal server error', + }) + + const response2 = await handleFetchRequest({ + router, + handlers: [ORPCHandler, OpenAPIServerHandler], + request: new Request('https://local.com/ping2', { method: 'POST' }), + }) + + expect(response2.status).toBe(488) + expect(await response2.json()).toEqual({ + code: 'PAYLOAD_TOO_LARGE', + status: 488, + message: '', + }) + }) + + it('input validation error', async () => { + const router = os.router({ + ping: os + .input(z.object({})) + .output(z.string()) + .func(() => { + return 'unnoq' + }), + }) + + const response = await handleFetchRequest({ + router, + handlers: [ORPCHandler, OpenAPIServerHandler], + request: new Request('https://local.com/ping', { method: 'POST' }), + }) + + expect(response.status).toBe(400) + expect(await response.json()).toEqual({ + code: 'BAD_REQUEST', + status: 400, + message: 'Validation input failed', + issues: [ + { + code: 'invalid_type', + expected: 'object', + message: 'Required', + path: [], + received: 'undefined', + }, + ], + }) + }) + + it('output validation error', async () => { + const router = os.router({ + ping: os + .input(z.string()) + .output(z.string()) + .func(() => { + return 12344 as any + }), + }) + + const response = await handleFetchRequest({ + router, + handlers: [ORPCHandler, OpenAPIServerHandler], + request: new Request('https://local.com/ping', { + method: 'POST', + body: '"hi"', + }), + }) + + expect(response.status).toBe(500) + expect(await response.json()).toEqual({ + code: 'INTERNAL_SERVER_ERROR', + status: 500, + message: 'Validation output failed', + }) + }) +}) + +describe('hooks', () => { + it('on success', async () => { + const onSuccess = vi.fn() + const onError = vi.fn() + const onFinish = vi.fn() + + await handleFetchRequest({ + router, + handlers: [ORPCHandler, OpenAPIServerHandler], + hooks: async (context, hooks) => { + try { + const response = await hooks.next() + onSuccess(response) + return response + } + catch (e) { + onError(e) + throw e + } + finally { + onFinish() + } + }, + prefix: '/orpc', + request: new Request('http://localhost/orpc/ping', { + method: 'POST', + }), + }) + + expect(onSuccess).toHaveBeenCalledTimes(1) + expect(onError).toHaveBeenCalledTimes(0) + expect(onFinish).toHaveBeenCalledTimes(1) + + expect(onSuccess.mock.calls[0]?.[0]).toBeInstanceOf(Response) + expect(onFinish.mock.calls[0]?.[1]).toBe(undefined) + }) + + it('on failed', async () => { + const onSuccess = vi.fn() + const onError = vi.fn() + const onFinish = vi.fn() + + await handleFetchRequest({ + router, + handlers: [ORPCHandler, OpenAPIServerHandler], + hooks: async (context, hooks) => { + try { + const response = await hooks.next() + onSuccess(response) + return response + } + catch (e) { + onError(e) + throw e + } + finally { + onFinish() + } + }, + prefix: '/orpc', + request: new Request('http://localhost/orpc/throw', { + method: 'POST', + }), + }) + + expect(onSuccess).toHaveBeenCalledTimes(0) + expect(onError).toHaveBeenCalledTimes(1) + expect(onFinish).toHaveBeenCalledTimes(1) + + expect(onError.mock.calls[0]?.[0]).toBeInstanceOf(Error) + expect(onError.mock.calls[0]?.[0]?.message).toBe('test') + expect(onFinish.mock.calls[0]?.[0]).toBe(undefined) + }) +}) + +describe('file upload', () => { + const router = os.router({ + signal: os.input(z.instanceof(Blob)).func((input) => { + return input + }), + multiple: os + .input( + z.object({ first: z.instanceof(Blob), second: z.instanceof(Blob) }), + ) + .func((input) => { + return input + }), + }) + + const blob1 = new Blob(['hello'], { type: 'text/plain;charset=utf-8' }) + const blob2 = new Blob(['"world"'], { type: 'image/png' }) + const blob3 = new Blob(['unnoq'], { type: 'application/octet-stream' }) + + it('single file', async () => { + const rForm = new FormData() + rForm.set('meta', JSON.stringify([])) + rForm.set('maps', JSON.stringify([[]])) + rForm.set('0', blob3) + + const response = await handleFetchRequest({ + router, + handlers: [ORPCHandler, OpenAPIServerHandler], + prefix: '/orpc', + request: new Request('http://localhost/orpc/signal', { + method: 'POST', + body: rForm, + headers: { + [ORPC_HEADER]: ORPC_HEADER_VALUE, + }, + }), + }) + + expect(response.status).toBe(200) + const form = await response.formData() + + const file0 = form.get('0') as File + expect(file0).toBeInstanceOf(File) + expect(file0.name).toBe('blob') + expect(file0.type).toBe('application/octet-stream') + expect(await file0.text()).toBe('unnoq') + }) + + it('multiple file', async () => { + const form = new FormData() + form.set('data', JSON.stringify({ first: blob1, second: blob2 })) + form.set('meta', JSON.stringify([])) + form.set('maps', JSON.stringify([['first'], ['second']])) + form.set('0', blob1) + form.set('1', blob2) + + const response = await handleFetchRequest({ + router, + handlers: [ORPCHandler, OpenAPIServerHandler], + prefix: '/orpc', + request: new Request('http://localhost/orpc/multiple', { + method: 'POST', + body: form, + headers: { + [ORPC_HEADER]: ORPC_HEADER_VALUE, + }, + }), + }) + + expect(response.status).toBe(200) + + const form_ = await response.formData() + const file0 = form_.get('0') as File + const file1 = form_.get('1') as File + + expect(file0).toBeInstanceOf(File) + expect(file0.name).toBe('blob') + expect(file0.type).toBe('text/plain;charset=utf-8') + expect(await file0.text()).toBe('hello') + + expect(file1).toBeInstanceOf(File) + expect(file1.name).toBe('blob') + expect(file1.type).toBe('image/png') + expect(await file1.text()).toBe('"world"') + }) +}) + +describe('accept header', () => { + const router = os.router({ + ping: os.func(async () => 'pong'), + }) + + it('application/json', async () => { + const response = await handleFetchRequest({ + router, + handlers: [ORPCHandler, OpenAPIServerHandler], + prefix: '/orpc', + request: new Request('http://localhost/orpc/ping', { + method: 'POST', + headers: { + Accept: 'application/json', + }, + }), + }) + + expect(response.headers.get('Content-Type')).toEqual('application/json') + + expect(await response.json()).toEqual('pong') + }) + + it('multipart/form-data', async () => { + const response = await handleFetchRequest({ + router, + handlers: [ORPCHandler, OpenAPIServerHandler], + prefix: '/orpc', + request: new Request('http://localhost/orpc/ping', { + method: 'POST', + headers: { + Accept: 'multipart/form-data', + }, + }), + }) + + expect(response.headers.get('Content-Type')).toContain( + 'multipart/form-data', + ) + + const form = await response.formData() + expect(form.get('')).toEqual('pong') + }) + + it('application/x-www-form-urlencoded', async () => { + const response = await handleFetchRequest({ + router, + handlers: [ORPCHandler, OpenAPIServerHandler], + prefix: '/orpc', + request: new Request('http://localhost/orpc/ping', { + method: 'POST', + headers: { + Accept: 'application/x-www-form-urlencoded', + }, + }), + }) + + expect(response.headers.get('Content-Type')).toEqual( + 'application/x-www-form-urlencoded', + ) + + const params = new URLSearchParams(await response.text()) + expect(params.get('')).toEqual('pong') + }) + + it('*/*', async () => { + const response = await handleFetchRequest({ + router, + handlers: [ORPCHandler, OpenAPIServerHandler], + prefix: '/orpc', + request: new Request('http://localhost/orpc/ping', { + method: 'POST', + headers: { + Accept: '*/*', + }, + }), + }) + + expect(response.headers.get('Content-Type')).toEqual('application/json') + expect(await response.json()).toEqual('pong') + }) + + it('invalid', async () => { + const response = await handleFetchRequest({ + router, + handlers: [ORPCHandler, OpenAPIServerHandler], + prefix: '/orpc', + request: new Request('http://localhost/orpc/ping', { + method: 'POST', + headers: { + Accept: 'invalid', + }, + }), + }) + + expect(response.headers.get('Content-Type')).toEqual('application/json') + expect(await response.json()).toEqual({ + code: 'NOT_ACCEPTABLE', + message: 'Unsupported content-type: invalid', + status: 406, + }) + }) +}) + +describe('dynamic params', () => { + const router = os.router({ + deep: os + .route({ + method: 'POST', + path: '/{id}/{id2}', + }) + .input( + z.object({ + id: z.number(), + id2: z.string(), + file: oz.file(), + }), + ) + .func(input => input), + + find: os + .route({ + method: 'GET', + path: '/{id}', + }) + .input( + z.object({ + id: z.number(), + }), + ) + .func(input => input), + }) + + const handlers = [ + { + router, + handlers: [ORPCHandler, OpenAPIServerHandler] as const, + }, + { + router, + handlers: [ORPCHandler, OpenAPIServerlessHandler] as const, + }, + ] + + it.each(handlers)('should handle dynamic params', async ({ router, handlers }) => { + const response = await handleFetchRequest({ + router, + handlers, + request: new Request('http://localhost/123'), + }) + + expect(response.status).toEqual(200) + expect(response.headers.get('Content-Type')).toEqual('application/json') + expect(await response.json()).toEqual({ id: 123 }) + }) + + it.each(handlers)('should handle deep dynamic params', async ({ handlers }) => { + const form = new FormData() + form.append('file', new Blob(['hello']), 'hello.txt') + + const response = await handleFetchRequest({ + router, + handlers, + request: new Request('http://localhost/123/dfdsfds', { + method: 'POST', + body: form, + }), + }) + + expect(response.status).toEqual(200) + const rForm = await response.formData() + expect(rForm.get('id')).toEqual('123') + expect(rForm.get('id2')).toEqual('dfdsfds') + }) +}) + +describe('can control method on POST request', () => { + const router = os.router({ + update: os + .route({ + method: 'PUT', + path: '/{id}', + }) + .input( + z.object({ + id: z.number(), + file: oz.file(), + }), + ) + .func(input => input), + }) + + const handlers = [ + [ORPCHandler, OpenAPIServerHandler], + [ORPCHandler, OpenAPIServerlessHandler], + ] as const + + it.each(handlers)('work', async (...handlers) => { + const form = new FormData() + form.set('file', new File(['hello'], 'hello.txt')) + + const response = await handleFetchRequest({ + router, + handlers, + request: new Request('http://localhost/123', { + method: 'POST', + body: form, + }), + }) + + expect(response.status).toEqual(404) + + const response2 = await handleFetchRequest({ + router, + handlers, + request: new Request('http://localhost/123?method=PUT', { + method: 'POST', + body: form, + }), + }) + + expect(response2.status).toEqual(200) + }) +}) diff --git a/packages/server/src/fetch/handle.ts b/packages/server/src/fetch/handle.ts index 80e5bd3be..8f6ea70b6 100644 --- a/packages/server/src/fetch/handle.ts +++ b/packages/server/src/fetch/handle.ts @@ -3,7 +3,7 @@ import type { FetchHandler, FetchHandlerOptions } from './types' import { ORPCError } from '@orpc/shared/error' export type HandleFetchRequestOptions> = FetchHandlerOptions & { - handlers: [FetchHandler, ...FetchHandler[]] + handlers: readonly [FetchHandler, ...FetchHandler[]] } export async function handleFetchRequest< TRouter extends Router>( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fefcb81ee..698789059 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -252,6 +252,10 @@ importers: zod: specifier: '>=3.23.0' version: 3.23.8 + devDependencies: + '@orpc/openapi': + specifier: workspace:* + version: link:../openapi packages/shared: dependencies: From 3804a1ba56b0732b16cd497cac9c42c536917bdd Mon Sep 17 00:00:00 2001 From: unnoq Date: Mon, 2 Dec 2024 19:15:09 +0700 Subject: [PATCH 09/11] use more flexible apis --- apps/content/content/docs/index.mdx | 9 +- apps/content/content/docs/server/context.mdx | 16 ++-- .../content/docs/server/integrations.mdx | 54 +++++++---- apps/content/examples/contract.ts | 9 +- apps/content/examples/server.ts | 9 +- packages/client/src/procedure.test.ts | 12 +-- packages/client/src/router.test.ts | 6 +- packages/openapi/src/fetch/server-handler.ts | 4 +- .../openapi/src/fetch/serverless-handler.ts | 4 +- packages/react/tests/orpc.tsx | 4 +- packages/server/src/fetch/handle.test.ts | 54 +++++------ .../src/fetch/{orpc-handler.ts => handler.ts} | 92 ++++++++++--------- packages/server/src/fetch/index.ts | 2 +- playgrounds/contract-openapi/src/main.ts | 6 +- playgrounds/expressjs/src/main.ts | 6 +- .../nextjs/src/app/api/[...rest]/route.ts | 6 +- playgrounds/openapi/src/main.ts | 6 +- 17 files changed, 168 insertions(+), 131 deletions(-) rename packages/server/src/fetch/{orpc-handler.ts => handler.ts} (50%) diff --git a/apps/content/content/docs/index.mdx b/apps/content/content/docs/index.mdx index 57b9e7aec..190f2660e 100644 --- a/apps/content/content/docs/index.mdx +++ b/apps/content/content/docs/index.mdx @@ -144,8 +144,8 @@ In oRPC middleware is very useful and fully typed you can find more info [here]( This example uses [@whatwg-node/server](https://www.npmjs.com/package/@whatwg-node/server) to create a Node server with [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). ```ts twoslash -import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' -import { OpenAPIServerlessHandler } from '@orpc/openapi/fetch' +import { handleFetchRequest, createORPCHandler } from '@orpc/server/fetch' +import { createOpenAPIServerlessHandler } from '@orpc/openapi/fetch' import { createServer } from 'node:http' import { createServerAdapter } from '@whatwg-node/server' import { router } from 'examples/server' @@ -160,7 +160,10 @@ const server = createServer( request, prefix: '/api', context: {}, - handlers: [ORPCHandler, OpenAPIServerlessHandler], + handlers: [ + createORPCHandler() + createOpenAPIServerlessHandler(), + ], }) } diff --git a/apps/content/content/docs/server/context.mdx b/apps/content/content/docs/server/context.mdx index d92eafaba..bb468f613 100644 --- a/apps/content/content/docs/server/context.mdx +++ b/apps/content/content/docs/server/context.mdx @@ -46,8 +46,6 @@ If your procedure only depends on `Middleware Context`, you can ```ts twoslash import { os, ORPCError } from '@orpc/server' -import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' -import { OpenAPIServerlessHandler, OpenAPIServerHandler } from '@orpc/openapi/fetch' import { headers } from 'next/headers' const base = os.use(async (input, context, meta) => { @@ -84,12 +82,18 @@ export const router = base.router({ // You can call this procedure directly without manually providing context const output = await router.getting() +import { handleFetchRequest, createORPCHandler } from '@orpc/server/fetch' +import { createOpenAPIServerlessHandler, createOpenAPIServerHandler } from '@orpc/openapi/fetch' + export function fetch(request: Request) { // No need to pass context; middleware handles it return handleFetchRequest({ router, request, - handlers: [ORPCHandler, OpenAPIServerlessHandler], + handlers: [ + createORPCHandler(), + createOpenAPIServerlessHandler() + ], }) } ``` @@ -102,8 +106,8 @@ rather than relying on global mechanisms like `headers` or `cookies` in Next.js. ```ts twoslash import { os, ORPCError, createProcedureCaller } from '@orpc/server' -import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' -import { OpenAPIServerlessHandler, OpenAPIServerHandler } from '@orpc/openapi/fetch' +import { handleFetchRequest, createORPCHandler } from '@orpc/server/fetch' +import { createOpenAPIServerlessHandler, createOpenAPIServerHandler } from '@orpc/openapi/fetch' type ORPCContext = { user?: { id: string }, db: 'fake-db' } @@ -138,7 +142,7 @@ export function fetch(request: Request) { router, request, context: { db, user }, - handlers: [ORPCHandler, OpenAPIServerlessHandler], + handlers: [createORPCHandler(), createOpenAPIServerlessHandler()], }) } diff --git a/apps/content/content/docs/server/integrations.mdx b/apps/content/content/docs/server/integrations.mdx index 2eed8f541..f27e71109 100644 --- a/apps/content/content/docs/server/integrations.mdx +++ b/apps/content/content/docs/server/integrations.mdx @@ -13,8 +13,8 @@ Whether you're targeting serverless, edge environments, or traditional backends, ## Quick Example ```ts twoslash -import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' -import { OpenAPIServerlessHandler, OpenAPIServerHandler } from '@orpc/openapi/fetch' +import { handleFetchRequest, createORPCHandler } from '@orpc/server/fetch' +import { createOpenAPIServerlessHandler, createOpenAPIServerHandler } from '@orpc/openapi/fetch' import { router } from 'examples/server' export function fetch(request: Request) { @@ -23,7 +23,10 @@ export function fetch(request: Request) { request, context: {}, // prefix: '/api', // Optionally define a route prefix - handlers: [ORPCHandler, OpenAPIServerlessHandler], + handlers: [ + createORPCHandler(), + createOpenAPIServerlessHandler(), + ], }) } ``` @@ -34,8 +37,8 @@ Node.js doesn't provide native support for creating server with [Fetch API](http but you can easily use [@whatwg-node/server](https://npmjs.com/package/@whatwg-node/server) as an adapter. ```ts twoslash -import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' -import { OpenAPIServerlessHandler, OpenAPIServerHandler } from '@orpc/openapi/fetch' +import { handleFetchRequest, createORPCHandler } from '@orpc/server/fetch' +import { createOpenAPIServerlessHandler, createOpenAPIServerHandler } from '@orpc/openapi/fetch' import { createServer } from 'node:http' import { createServerAdapter } from '@whatwg-node/server' import { router } from 'examples/server' @@ -47,7 +50,10 @@ const server = createServer( request, context: {}, // prefix: '/api', - handlers: [ORPCHandler, OpenAPIServerHandler], + handlers: [ + createORPCHandler(), + createOpenAPIServerlessHandler(), + ], }) }) ) @@ -60,8 +66,8 @@ server.listen(3000, () => { ## Express.js ```ts twoslash -import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' -import { OpenAPIServerlessHandler, OpenAPIServerHandler } from '@orpc/openapi/fetch' +import { handleFetchRequest, createORPCHandler } from '@orpc/server/fetch' +import { createOpenAPIServerlessHandler, createOpenAPIServerHandler } from '@orpc/openapi/fetch' import { createServerAdapter } from '@whatwg-node/server' import express from 'express' import { router } from 'examples/server' @@ -74,7 +80,10 @@ app.all('/api/*', createServerAdapter((request: Request) => { request, context: {}, prefix: '/api', - handlers: [ORPCHandler, OpenAPIServerHandler], + handlers: [ + createORPCHandler(), + createOpenAPIServerHandler(), + ], }) })) @@ -87,8 +96,8 @@ app.listen(3000, () => { ```ts twoslash import { Hono } from 'hono' -import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' -import { OpenAPIServerlessHandler, OpenAPIServerHandler } from '@orpc/openapi/fetch' +import { handleFetchRequest, createORPCHandler } from '@orpc/server/fetch' +import { createOpenAPIServerlessHandler, createOpenAPIServerHandler } from '@orpc/openapi/fetch' import { router } from 'examples/server' const app = new Hono() @@ -99,7 +108,10 @@ app.get('/api/*', (c) => { request: c.req.raw, prefix: '/api', context: {}, - handlers: [ORPCHandler, OpenAPIServerlessHandler], + handlers: [ + createORPCHandler(), + createOpenAPIServerlessHandler(), + ], }) }) @@ -109,8 +121,8 @@ export default app ## Next.js ```ts title="app/api/[...orpc]/route.ts" twoslash -import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' -import { OpenAPIServerlessHandler, OpenAPIServerHandler } from '@orpc/openapi/fetch' +import { handleFetchRequest, createORPCHandler } from '@orpc/server/fetch' +import { createOpenAPIServerlessHandler, createOpenAPIServerHandler } from '@orpc/openapi/fetch' import { router } from 'examples/server' export function GET(request: Request) { @@ -119,7 +131,10 @@ export function GET(request: Request) { request, prefix: '/api', context: {}, - handlers: [ORPCHandler, OpenAPIServerlessHandler], + handlers: [ + createORPCHandler(), + createOpenAPIServerlessHandler(), + ], }) } @@ -132,8 +147,8 @@ export const PATCH = GET ## Cloudflare Workers ```ts twoslash -import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' -import { OpenAPIServerlessHandler, OpenAPIServerHandler } from '@orpc/openapi/fetch' +import { handleFetchRequest, createORPCHandler } from '@orpc/server/fetch' +import { createOpenAPIServerlessHandler, createOpenAPIServerHandler } from '@orpc/openapi/fetch' import { router } from 'examples/server' export default { @@ -143,7 +158,10 @@ export default { request, prefix: '/', context: {}, - handlers: [ORPCHandler, OpenAPIServerlessHandler], + handlers: [ + createORPCHandler(), + createOpenAPIServerlessHandler(), + ], }) }, } diff --git a/apps/content/examples/contract.ts b/apps/content/examples/contract.ts index bcd20c181..ccd0afd9f 100644 --- a/apps/content/examples/contract.ts +++ b/apps/content/examples/contract.ts @@ -115,8 +115,8 @@ export const router = pub.router({ }) // Expose apis to the internet with fetch handler -import { OpenAPIServerlessHandler } from '@orpc/openapi/fetch' -import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' +import { createOpenAPIServerlessHandler } from '@orpc/openapi/fetch' +import { createORPCHandler, handleFetchRequest } from '@orpc/server/fetch' // Modern runtime that support fetch api like deno, bun, cloudflare workers, even node can used import { createServer } from 'node:http' import { createServerAdapter } from '@whatwg-node/server' @@ -131,7 +131,10 @@ const server = createServer( request, prefix: '/api', context: {}, - handlers: [ORPCHandler, OpenAPIServerlessHandler], + handlers: [ + createORPCHandler(), + createOpenAPIServerlessHandler(), + ], }) } diff --git a/apps/content/examples/server.ts b/apps/content/examples/server.ts index 9db135ff0..c7a7be7ec 100644 --- a/apps/content/examples/server.ts +++ b/apps/content/examples/server.ts @@ -89,8 +89,8 @@ export type Inputs = InferRouterInputs export type Outputs = InferRouterOutputs // Expose apis to the internet with fetch handler -import { OpenAPIServerlessHandler } from '@orpc/openapi/fetch' -import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' +import { createOpenAPIServerlessHandler } from '@orpc/openapi/fetch' +import { createORPCHandler, handleFetchRequest } from '@orpc/server/fetch' // Modern runtime that support fetch api like deno, bun, cloudflare workers, even node can used import { createServer } from 'node:http' import { createServerAdapter } from '@whatwg-node/server' @@ -105,7 +105,10 @@ const server = createServer( request, prefix: '/api', context: {}, - handlers: [ORPCHandler, OpenAPIServerlessHandler], + handlers: [ + createORPCHandler(), + createOpenAPIServerlessHandler(), + ], }) } diff --git a/packages/client/src/procedure.test.ts b/packages/client/src/procedure.test.ts index 3b82d33e1..3ef81088a 100644 --- a/packages/client/src/procedure.test.ts +++ b/packages/client/src/procedure.test.ts @@ -1,6 +1,6 @@ -import { OpenAPIServerHandler, OpenAPIServerlessHandler } from '@orpc/openapi/fetch' +import { createOpenAPIServerHandler, createOpenAPIServerlessHandler } from '@orpc/openapi/fetch' import { ORPCError, os } from '@orpc/server' -import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' +import { createORPCHandler, handleFetchRequest } from '@orpc/server/fetch' import { z } from 'zod' import { createProcedureClient } from './procedure' @@ -20,7 +20,7 @@ describe('createProcedureClient', () => { const request = new Request(...args) const response = await handleFetchRequest({ router, - handlers: [OpenAPIServerHandler, OpenAPIServerlessHandler, ORPCHandler], // make sure still work with openapi handlers + handlers: [createOpenAPIServerHandler(), createOpenAPIServerlessHandler(), createORPCHandler()], // make sure still work with openapi handlers prefix: '/orpc', request, context: {}, @@ -122,7 +122,7 @@ describe('createProcedureClient', () => { const request = new Request(...args) return handleFetchRequest({ router, - handlers: [ORPCHandler, OpenAPIServerHandler], + handlers: [createORPCHandler(), createOpenAPIServerHandler()], prefix: '/orpc', request, context: {}, @@ -153,7 +153,7 @@ describe('createProcedureClient', () => { const request = new Request(...args) return handleFetchRequest({ router, - handlers: [ORPCHandler], + handlers: [createORPCHandler()], prefix: '/orpc', request, context: {}, @@ -193,7 +193,7 @@ describe('upload file', () => { const request = new Request(...args) return handleFetchRequest({ router, - handlers: [ORPCHandler], + handlers: [createORPCHandler()], prefix: '/orpc', request, context: {}, diff --git a/packages/client/src/router.test.ts b/packages/client/src/router.test.ts index 882a91ba9..8a04761a2 100644 --- a/packages/client/src/router.test.ts +++ b/packages/client/src/router.test.ts @@ -1,6 +1,6 @@ import { oc } from '@orpc/contract' import { os } from '@orpc/server' -import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' +import { createORPCHandler, handleFetchRequest } from '@orpc/server/fetch' import { z } from 'zod' import { createRouterClient } from './router' @@ -22,7 +22,7 @@ describe('createRouterClient', () => { prefix: '/orpc', request, context: {}, - handlers: [ORPCHandler], + handlers: [createORPCHandler()], }) } @@ -134,7 +134,7 @@ describe('createRouterClient', () => { prefix: '/orpc', request, context: {}, - handlers: [ORPCHandler], + handlers: [createORPCHandler()], }) }, }) diff --git a/packages/openapi/src/fetch/server-handler.ts b/packages/openapi/src/fetch/server-handler.ts index 32b41fd63..687688d80 100644 --- a/packages/openapi/src/fetch/server-handler.ts +++ b/packages/openapi/src/fetch/server-handler.ts @@ -2,4 +2,6 @@ import type { FetchHandler } from '@orpc/server/fetch' import { RegExpRouter } from 'hono/router/reg-exp-router' import { createOpenAPIHandler } from './base-handler' -export const OpenAPIServerHandler: FetchHandler = createOpenAPIHandler(() => new RegExpRouter()) +export function createOpenAPIServerHandler(): FetchHandler { + return createOpenAPIHandler(() => new RegExpRouter()) +} diff --git a/packages/openapi/src/fetch/serverless-handler.ts b/packages/openapi/src/fetch/serverless-handler.ts index 8d2bd17e7..784c45a94 100644 --- a/packages/openapi/src/fetch/serverless-handler.ts +++ b/packages/openapi/src/fetch/serverless-handler.ts @@ -2,4 +2,6 @@ import type { FetchHandler } from '@orpc/server/fetch' import { LinearRouter } from 'hono/router/linear-router' import { createOpenAPIHandler } from './base-handler' -export const OpenAPIServerlessHandler: FetchHandler = createOpenAPIHandler(() => new LinearRouter()) +export function createOpenAPIServerlessHandler(): FetchHandler { + return createOpenAPIHandler(() => new LinearRouter()) +} diff --git a/packages/react/tests/orpc.tsx b/packages/react/tests/orpc.tsx index 860a27d38..3dc6cdb6a 100644 --- a/packages/react/tests/orpc.tsx +++ b/packages/react/tests/orpc.tsx @@ -1,6 +1,6 @@ import { createORPCClient } from '@orpc/client' import { os } from '@orpc/server' -import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' +import { createORPCHandler, handleFetchRequest } from '@orpc/server/fetch' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import React, { Suspense } from 'react' import { z } from 'zod' @@ -102,7 +102,7 @@ export const orpcClient = createORPCClient({ return handleFetchRequest({ router: appRouter, request, - handlers: [ORPCHandler], + handlers: [createORPCHandler()], }) }, }) diff --git a/packages/server/src/fetch/handle.test.ts b/packages/server/src/fetch/handle.test.ts index a01d3a568..d5eca8e98 100644 --- a/packages/server/src/fetch/handle.test.ts +++ b/packages/server/src/fetch/handle.test.ts @@ -1,11 +1,11 @@ import { ORPC_HEADER, ORPC_HEADER_VALUE } from '@orpc/contract' -import { OpenAPIServerHandler, OpenAPIServerlessHandler } from '@orpc/openapi/fetch' +import { createOpenAPIServerHandler, createOpenAPIServerlessHandler } from '@orpc/openapi/fetch' import { oz } from '@orpc/zod' import { describe, expect, it } from 'vitest' import { z } from 'zod' import { ORPCError, os } from '..' import { handleFetchRequest } from './handle' -import { ORPCHandler } from './orpc-handler' +import { createORPCHandler } from './handler' const router = os.router({ throw: os.func(() => { @@ -31,7 +31,7 @@ describe('simple', () => { it('200: public url', async () => { const response = await handleFetchRequest({ router, - handlers: [ORPCHandler, OpenAPIServerlessHandler], + handlers: [createORPCHandler(), createOpenAPIServerlessHandler()], prefix: '/orpc', request: new Request('http://localhost/orpc/ping', { method: 'POST', @@ -44,7 +44,7 @@ describe('simple', () => { const response2 = await handleFetchRequest({ router, - handlers: [ORPCHandler, OpenAPIServerlessHandler], + handlers: [createORPCHandler(), createOpenAPIServerlessHandler()], prefix: '/orpc', request: new Request('http://localhost/orpc/ping2', { method: 'GET', @@ -59,7 +59,7 @@ describe('simple', () => { it('200: internal url', async () => { const response = await handleFetchRequest({ router, - handlers: [ORPCHandler, OpenAPIServerlessHandler], + handlers: [createORPCHandler(), createOpenAPIServerlessHandler()], request: new Request('http://localhost/ping', { method: 'POST', }), @@ -71,7 +71,7 @@ describe('simple', () => { const response2 = await handleFetchRequest({ router, - handlers: [ORPCHandler, OpenAPIServerlessHandler], + handlers: [createORPCHandler(), createOpenAPIServerlessHandler()], prefix: '/orpc', request: new Request('http://localhost/orpc/ping2'), context: { auth: true }, @@ -84,7 +84,7 @@ describe('simple', () => { it('404', async () => { const response = await handleFetchRequest({ router, - handlers: [ORPCHandler, OpenAPIServerHandler], + handlers: [createORPCHandler(), createOpenAPIServerHandler()], prefix: '/orpc', request: new Request('http://localhost/orpc/pingp', { method: 'POST', @@ -100,7 +100,7 @@ describe('procedure throw error', () => { it('unknown error', async () => { const response = await handleFetchRequest({ router, - handlers: [ORPCHandler, OpenAPIServerHandler], + handlers: [createORPCHandler(), createOpenAPIServerHandler()], request: new Request('https://local.com/throw', { method: 'POST' }), }) @@ -121,7 +121,7 @@ describe('procedure throw error', () => { const response = await handleFetchRequest({ router, - handlers: [ORPCHandler, OpenAPIServerHandler], + handlers: [createORPCHandler(), createOpenAPIServerHandler()], request: new Request('https://local.com/ping', { method: 'POST' }), }) @@ -146,7 +146,7 @@ describe('procedure throw error', () => { const response = await handleFetchRequest({ router, - handlers: [ORPCHandler, OpenAPIServerHandler], + handlers: [createORPCHandler(), createOpenAPIServerHandler()], request: new Request('https://local.com/ping', { method: 'POST' }), }) @@ -178,7 +178,7 @@ describe('procedure throw error', () => { const response = await handleFetchRequest({ router, - handlers: [ORPCHandler, OpenAPIServerHandler], + handlers: [createORPCHandler(), createOpenAPIServerHandler()], request: new Request('https://local.com/ping', { method: 'POST' }), }) @@ -191,7 +191,7 @@ describe('procedure throw error', () => { const response2 = await handleFetchRequest({ router, - handlers: [ORPCHandler, OpenAPIServerHandler], + handlers: [createORPCHandler(), createOpenAPIServerHandler()], request: new Request('https://local.com/ping2', { method: 'POST' }), }) @@ -215,7 +215,7 @@ describe('procedure throw error', () => { const response = await handleFetchRequest({ router, - handlers: [ORPCHandler, OpenAPIServerHandler], + handlers: [createORPCHandler(), createOpenAPIServerHandler()], request: new Request('https://local.com/ping', { method: 'POST' }), }) @@ -248,7 +248,7 @@ describe('procedure throw error', () => { const response = await handleFetchRequest({ router, - handlers: [ORPCHandler, OpenAPIServerHandler], + handlers: [createORPCHandler(), createOpenAPIServerHandler()], request: new Request('https://local.com/ping', { method: 'POST', body: '"hi"', @@ -272,7 +272,7 @@ describe('hooks', () => { await handleFetchRequest({ router, - handlers: [ORPCHandler, OpenAPIServerHandler], + handlers: [createORPCHandler(), createOpenAPIServerHandler()], hooks: async (context, hooks) => { try { const response = await hooks.next() @@ -308,7 +308,7 @@ describe('hooks', () => { await handleFetchRequest({ router, - handlers: [ORPCHandler, OpenAPIServerHandler], + handlers: [createORPCHandler(), createOpenAPIServerHandler()], hooks: async (context, hooks) => { try { const response = await hooks.next() @@ -365,7 +365,7 @@ describe('file upload', () => { const response = await handleFetchRequest({ router, - handlers: [ORPCHandler, OpenAPIServerHandler], + handlers: [createORPCHandler(), createOpenAPIServerHandler()], prefix: '/orpc', request: new Request('http://localhost/orpc/signal', { method: 'POST', @@ -396,7 +396,7 @@ describe('file upload', () => { const response = await handleFetchRequest({ router, - handlers: [ORPCHandler, OpenAPIServerHandler], + handlers: [createORPCHandler(), createOpenAPIServerHandler()], prefix: '/orpc', request: new Request('http://localhost/orpc/multiple', { method: 'POST', @@ -433,7 +433,7 @@ describe('accept header', () => { it('application/json', async () => { const response = await handleFetchRequest({ router, - handlers: [ORPCHandler, OpenAPIServerHandler], + handlers: [createORPCHandler(), createOpenAPIServerHandler()], prefix: '/orpc', request: new Request('http://localhost/orpc/ping', { method: 'POST', @@ -451,7 +451,7 @@ describe('accept header', () => { it('multipart/form-data', async () => { const response = await handleFetchRequest({ router, - handlers: [ORPCHandler, OpenAPIServerHandler], + handlers: [createORPCHandler(), createOpenAPIServerHandler()], prefix: '/orpc', request: new Request('http://localhost/orpc/ping', { method: 'POST', @@ -472,7 +472,7 @@ describe('accept header', () => { it('application/x-www-form-urlencoded', async () => { const response = await handleFetchRequest({ router, - handlers: [ORPCHandler, OpenAPIServerHandler], + handlers: [createORPCHandler(), createOpenAPIServerHandler()], prefix: '/orpc', request: new Request('http://localhost/orpc/ping', { method: 'POST', @@ -493,7 +493,7 @@ describe('accept header', () => { it('*/*', async () => { const response = await handleFetchRequest({ router, - handlers: [ORPCHandler, OpenAPIServerHandler], + handlers: [createORPCHandler(), createOpenAPIServerHandler()], prefix: '/orpc', request: new Request('http://localhost/orpc/ping', { method: 'POST', @@ -510,7 +510,7 @@ describe('accept header', () => { it('invalid', async () => { const response = await handleFetchRequest({ router, - handlers: [ORPCHandler, OpenAPIServerHandler], + handlers: [createORPCHandler(), createOpenAPIServerHandler()], prefix: '/orpc', request: new Request('http://localhost/orpc/ping', { method: 'POST', @@ -561,11 +561,11 @@ describe('dynamic params', () => { const handlers = [ { router, - handlers: [ORPCHandler, OpenAPIServerHandler] as const, + handlers: [createORPCHandler(), createOpenAPIServerHandler()] as const, }, { router, - handlers: [ORPCHandler, OpenAPIServerlessHandler] as const, + handlers: [createORPCHandler(), createOpenAPIServerlessHandler()] as const, }, ] @@ -618,8 +618,8 @@ describe('can control method on POST request', () => { }) const handlers = [ - [ORPCHandler, OpenAPIServerHandler], - [ORPCHandler, OpenAPIServerlessHandler], + [createORPCHandler(), createOpenAPIServerHandler()], + [createORPCHandler(), createOpenAPIServerlessHandler()], ] as const it.each(handlers)('work', async (...handlers) => { diff --git a/packages/server/src/fetch/orpc-handler.ts b/packages/server/src/fetch/handler.ts similarity index 50% rename from packages/server/src/fetch/orpc-handler.ts rename to packages/server/src/fetch/handler.ts index 0e10ec869..2ef7b2bdd 100644 --- a/packages/server/src/fetch/orpc-handler.ts +++ b/packages/server/src/fetch/handler.ts @@ -11,62 +11,64 @@ import { createProcedureCaller } from '../procedure-caller' const serializer = new ORPCSerializer() const deserializer = new ORPCDeserializer() -export const ORPCHandler: FetchHandler = async (options) => { - if (options.request.headers.get(ORPC_HEADER) !== ORPC_HEADER_VALUE) { - return undefined - } +export function createORPCHandler(): FetchHandler { + return async (options) => { + if (options.request.headers.get(ORPC_HEADER) !== ORPC_HEADER_VALUE) { + return undefined + } - const context = await value(options.context) + const context = await value(options.context) - const handler = async () => { - const url = new URL(options.request.url) - const pathname = `/${trim(url.pathname.replace(options.prefix ?? '', ''), '/')}` + const handler = async () => { + const url = new URL(options.request.url) + const pathname = `/${trim(url.pathname.replace(options.prefix ?? '', ''), '/')}` - const match = resolveORPCRouter(options.router, pathname) + const match = resolveORPCRouter(options.router, pathname) - if (!match) { - throw new ORPCError({ code: 'NOT_FOUND', message: 'Not found' }) - } + if (!match) { + throw new ORPCError({ code: 'NOT_FOUND', message: 'Not found' }) + } - const input = await deserializeRequest(options.request) + const input = await deserializeRequest(options.request) - const caller = createProcedureCaller({ - context, - procedure: match.procedure, - path: match.path, - }) - - const output = await caller(input) + const caller = createProcedureCaller({ + context, + procedure: match.procedure, + path: match.path, + }) - const { body, headers } = serializer.serialize(output) + const output = await caller(input) - return new Response(body, { - status: 200, - headers, - }) - } + const { body, headers } = serializer.serialize(output) - try { - return await options.hooks?.( - context as any, - { next: handler, response: response => response }, - ) ?? await handler() - } - catch (e) { - const error = e instanceof ORPCError - ? e - : new ORPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: 'Internal server error', - cause: e, + return new Response(body, { + status: 200, + headers, }) + } - const { body, headers } = serializer.serialize(error.toJSON()) - - return new Response(body, { - status: error.status, - headers, - }) + try { + return await options.hooks?.( + context as any, + { next: handler, response: response => response }, + ) ?? await handler() + } + 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, + }) + } } } diff --git a/packages/server/src/fetch/index.ts b/packages/server/src/fetch/index.ts index 31f37a702..969d7042e 100644 --- a/packages/server/src/fetch/index.ts +++ b/packages/server/src/fetch/index.ts @@ -1,3 +1,3 @@ export * from './handle' -export * from './orpc-handler' +export * from './handler' export * from './types' diff --git a/playgrounds/contract-openapi/src/main.ts b/playgrounds/contract-openapi/src/main.ts index 54a493061..f780d3f94 100644 --- a/playgrounds/contract-openapi/src/main.ts +++ b/playgrounds/contract-openapi/src/main.ts @@ -1,7 +1,7 @@ import { createServer } from 'node:http' import { generateOpenAPI } from '@orpc/openapi' -import { OpenAPIServerHandler } from '@orpc/openapi/fetch' -import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' +import { createOpenAPIServerHandler } from '@orpc/openapi/fetch' +import { createORPCHandler, handleFetchRequest } from '@orpc/server/fetch' import { createServerAdapter } from '@whatwg-node/server' import { contract } from './contract' import { router } from './router' @@ -21,7 +21,7 @@ const server = createServer( request, prefix: '/api', context, - handlers: [ORPCHandler, OpenAPIServerHandler], + handlers: [createORPCHandler(), createOpenAPIServerHandler()], async hooks(context, hooks) { try { return hooks.next() diff --git a/playgrounds/expressjs/src/main.ts b/playgrounds/expressjs/src/main.ts index 595e5367b..3c8da22b1 100644 --- a/playgrounds/expressjs/src/main.ts +++ b/playgrounds/expressjs/src/main.ts @@ -1,6 +1,6 @@ import { generateOpenAPI } from '@orpc/openapi' -import { OpenAPIServerHandler } from '@orpc/openapi/fetch' -import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' +import { createOpenAPIServerHandler } from '@orpc/openapi/fetch' +import { createORPCHandler, handleFetchRequest } from '@orpc/server/fetch' import { createServerAdapter } from '@whatwg-node/server' import express from 'express' import { router } from './router' @@ -20,7 +20,7 @@ app.all( prefix: '/api', context, router, - handlers: [ORPCHandler, OpenAPIServerHandler], + handlers: [createORPCHandler(), createOpenAPIServerHandler()], async hooks(context, hooks) { try { return hooks.next() diff --git a/playgrounds/nextjs/src/app/api/[...rest]/route.ts b/playgrounds/nextjs/src/app/api/[...rest]/route.ts index eeacc4110..bac44f538 100644 --- a/playgrounds/nextjs/src/app/api/[...rest]/route.ts +++ b/playgrounds/nextjs/src/app/api/[...rest]/route.ts @@ -1,5 +1,5 @@ -import { OpenAPIServerHandler } from '@orpc/openapi/fetch' -import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' +import { createOpenAPIServerHandler } from '@orpc/openapi/fetch' +import { createORPCHandler, handleFetchRequest } from '@orpc/server/fetch' import { router } from './router' export function GET(request: Request) { @@ -7,7 +7,7 @@ export function GET(request: Request) { router, request, prefix: '/api', - handlers: [ORPCHandler, OpenAPIServerHandler], + handlers: [createORPCHandler(), createOpenAPIServerHandler()], }) } diff --git a/playgrounds/openapi/src/main.ts b/playgrounds/openapi/src/main.ts index b485d05f7..facf3e4ad 100644 --- a/playgrounds/openapi/src/main.ts +++ b/playgrounds/openapi/src/main.ts @@ -1,7 +1,7 @@ import { createServer } from 'node:http' import { generateOpenAPI } from '@orpc/openapi' -import { OpenAPIServerHandler } from '@orpc/openapi/fetch' -import { handleFetchRequest, ORPCHandler } from '@orpc/server/fetch' +import { createOpenAPIServerHandler } from '@orpc/openapi/fetch' +import { createORPCHandler, handleFetchRequest } from '@orpc/server/fetch' import { createServerAdapter } from '@whatwg-node/server' import { router } from './router' import './polyfill' @@ -20,7 +20,7 @@ const server = createServer( prefix: '/api', context, router, - handlers: [ORPCHandler, OpenAPIServerHandler], + handlers: [createORPCHandler(), createOpenAPIServerHandler()], async hooks(context, hooks) { try { return hooks.next() From d0c9e7314f77b8f345bbc29b28e4bee650c6e61d Mon Sep 17 00:00:00 2001 From: unnoq Date: Mon, 2 Dec 2024 19:20:11 +0700 Subject: [PATCH 10/11] fix caching --- packages/openapi/src/fetch/base-handler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/openapi/src/fetch/base-handler.ts b/packages/openapi/src/fetch/base-handler.ts index e98fa6efa..2b600077a 100644 --- a/packages/openapi/src/fetch/base-handler.ts +++ b/packages/openapi/src/fetch/base-handler.ts @@ -106,9 +106,9 @@ export function createOpenAPIHandler(createHonoRouter: () => Routing): FetchHand } } -export function createResolveRouter(createHonoRouter: () => Routing): ResolveRouter { - const routingCache = new Map, Routing>() +const routingCache = new Map, Routing>() +export function createResolveRouter(createHonoRouter: () => Routing): ResolveRouter { return (router: Router, method: string, pathname: string) => { let routing = routingCache.get(router) From c8f1c5e9a8a571792caca00c4f638bd04a779555 Mon Sep 17 00:00:00 2001 From: unnoq Date: Mon, 2 Dec 2024 19:25:50 +0700 Subject: [PATCH 11/11] docs fixed --- apps/content/content/docs/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/content/content/docs/index.mdx b/apps/content/content/docs/index.mdx index 190f2660e..6668df92c 100644 --- a/apps/content/content/docs/index.mdx +++ b/apps/content/content/docs/index.mdx @@ -161,7 +161,7 @@ const server = createServer( prefix: '/api', context: {}, handlers: [ - createORPCHandler() + createORPCHandler(), createOpenAPIServerlessHandler(), ], })