Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(server)!: improve context #96

Merged
merged 7 commits into from
Jan 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/content/content/docs/server/lazy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Here's how you can set up and use them:
```typescript twoslash
import { os, call } from '@orpc/server'

const pub = os.context<{ user?: { id: string } } | undefined>()
const pub = os.context<{ user?: { id: string } }>()

// Define a router with lazy loading
const router = pub.router({
Expand Down
2 changes: 1 addition & 1 deletion apps/content/content/docs/server/server-action.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ When calling `.actionable()`, you can pass a context function that provides addi
import { os } from '@orpc/server'
import { z } from 'zod'

const pub = os.context<{ db: string } | undefined>()
const pub = os.context<{ db: string }>()

export const getting = pub
.input(z.object({
Expand Down
4 changes: 2 additions & 2 deletions apps/content/examples/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { ORPCError, os } from '@orpc/server'
import { oz, ZodCoercer } from '@orpc/zod'
import { z } from 'zod'

export type Context = { user?: { id: string } } | undefined
export type Context = { user?: { id: string } }

// global pub, authed completely optional
export const pub = os.context<Context>()
export const authed = pub.use(({ context, path, next }, input) => {
/** put auth logic here */
return next({})
return next()
})

export const router = pub.router({
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi/src/adapters/fetch/openapi-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class OpenAPIHandler<T extends Context> implements FetchHandler<T> {
}

async handle(request: Request, ...[options]: FetchHandleRest<T>): Promise<FetchHandleResult> {
const context = options?.context as T
const context = options?.context ?? {} as T
const headers = request.headers
const accept = headers.get('accept') || undefined

Expand Down
4 changes: 2 additions & 2 deletions packages/server/src/adapters/fetch/orpc-handler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Hooks } from '@orpc/shared'
import type { Context } from '../../context'
import type { Router } from '../../router'
import type { Context } from '../../types'
import type { FetchHandler, FetchHandleRest, FetchHandleResult } from './types'
import { ORPCError } from '@orpc/contract'
import { executeWithHooks, trim } from '@orpc/shared'
Expand All @@ -25,7 +25,7 @@ export class RPCHandler<T extends Context> implements FetchHandler<T> {
}

async handle(request: Request, ...[options]: FetchHandleRest<T>): Promise<FetchHandleResult> {
const context = options?.context as T
const context = options?.context ?? {} as T

const execute = async (): Promise<FetchHandleResult> => {
const url = new URL(request.url)
Expand Down
4 changes: 2 additions & 2 deletions packages/server/src/adapters/fetch/types.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { FetchHandler } from './types'

describe('FetchHandler', () => {
it('optional context when context is undefinable', () => {
const handler = {} as FetchHandler<{ auth: boolean } | undefined>
it('optional context when all context is optional', () => {
const handler = {} as FetchHandler<{ auth?: boolean }>

handler.handle(new Request('https://example.com'))
handler.handle(new Request('https://example.com'), { context: { auth: true } })
Expand Down
9 changes: 6 additions & 3 deletions packages/server/src/adapters/fetch/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import type { HTTPPath } from '@orpc/contract'
import type { Context } from '../../types'
import type { Context } from '../../context'

export type FetchHandleOptions<T extends Context> =
& { prefix?: HTTPPath }
& (undefined extends T ? { context?: T } : { context: T })
& (Record<never, never> extends T ? { context?: T } : { context: T })

export type FetchHandleRest<T extends Context> =
| [options: FetchHandleOptions<T>]
| (Record<never, never> extends T ? [] : never)

export type FetchHandleRest<T extends Context> = [options: FetchHandleOptions<T>] | (undefined extends T ? [] : never)
export type FetchHandleResult = { matched: true, response: Response } | { matched: false, response: undefined }

export interface FetchHandler<T extends Context> {
Expand Down
12 changes: 7 additions & 5 deletions packages/server/src/adapters/hono/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import type { Context as HonoContext, MiddlewareHandler } from 'hono'
import type { Context } from '../../types'
import type { Context } from '../../context'
import type { FetchHandleOptions, FetchHandler } from '../fetch'
import { value, type Value } from '@orpc/shared'

export type CreateMiddlewareOptions<T extends Context> =
& Omit<FetchHandleOptions<T>, 'context'>
& (undefined extends T ? { context?: Value<T, [HonoContext]> } : { context: Value<T, [HonoContext]> })
& (Record<never, never> extends T ? { context?: Value<T, [HonoContext]> } : { context: Value<T, [HonoContext]> })

export type CreateMiddlewareRest<T extends Context> = [options: CreateMiddlewareOptions<T>] | (undefined extends T ? [] : never)
export type CreateMiddlewareRest<T extends Context> =
| [options: CreateMiddlewareOptions<T>]
| (Record<never, never> extends T ? [] : never)

export function createMiddleware<T extends Context>(handler: FetchHandler<T>, ...[options]: CreateMiddlewareRest<T>): MiddlewareHandler {
return async (c, next) => {
const context = await value(options?.context, c)
const context = await value(options?.context ?? {}, c) as any

const { matched, response } = await handler.handle(c.req.raw, { ...options, context } as any)
const { matched, response } = await handler.handle(c.req.raw, { ...options, context })

if (matched) {
c.res = response
Expand Down
10 changes: 6 additions & 4 deletions packages/server/src/adapters/next/serve.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import type { NextRequest } from 'next/server'
import type { Context } from '../../types'
import type { Context } from '../../context'
import type { FetchHandleOptions, FetchHandler } from '../fetch'
import { value, type Value } from '@orpc/shared'

export type ServeOptions<T extends Context> =
& Omit<FetchHandleOptions<T>, 'context'>
& (undefined extends T ? { context?: Value<T, [NextRequest]> } : { context: Value<T, [NextRequest]> })
& (Record<never, never> extends T ? { context?: Value<T, [NextRequest]> } : { context: Value<T, [NextRequest]> })

export type ServeRest<T extends Context> = [options: ServeOptions<T>] | (undefined extends T ? [] : never)
export type ServeRest<T extends Context> =
| [options: ServeOptions<T>]
| (Record<never, never> extends T ? [] : never)

export interface ServeResult {
GET: (req: NextRequest) => Promise<Response>
Expand All @@ -19,7 +21,7 @@ export interface ServeResult {

export function serve<T extends Context>(handler: FetchHandler<T>, ...[options]: ServeRest<T>): ServeResult {
const main = async (req: NextRequest) => {
const context = await value(options?.context, req) as any
const context = await value(options?.context ?? {}, req) as any

const { matched, response } = await handler.handle(req, { ...options, context })

Expand Down
12 changes: 6 additions & 6 deletions packages/server/src/adapters/node/orpc-handler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ServerResponse } from 'node:http'
import type { Context } from '../../context'
import type { Router } from '../../router'
import type { Context } from '../../types'
import type { RPCHandlerOptions } from '../fetch/orpc-handler'
import type { RequestHandler, RequestHandleRest, RequestHandleResult } from './types'
import { RPCHandler as ORPCFetchHandler } from '../fetch/orpc-handler'
Expand All @@ -13,18 +13,18 @@ export class RPCHandler<T extends Context> implements RequestHandler<T> {
this.orpcFetchHandler = new ORPCFetchHandler(router, options)
}

async handle(req: ExpressableIncomingMessage, res: ServerResponse, ...[options]: RequestHandleRest<T>): Promise<RequestHandleResult> {
async handle(req: ExpressableIncomingMessage, res: ServerResponse, ...rest: RequestHandleRest<T>): Promise<RequestHandleResult> {
const request = createRequest(req, res)

const castedOptions = (options ?? {}) as Exclude<typeof options, undefined>

const result = await this.orpcFetchHandler.handle(request, castedOptions)
const result = await this.orpcFetchHandler.handle(request, ...rest)

if (result.matched === false) {
return { matched: false }
}

await options?.beforeSend?.(result.response, castedOptions.context as T)
const context = rest[0]?.context ?? {} as T

await rest[0]?.beforeSend?.(result.response, context)

await sendResponse(res, result.response)

Expand Down
8 changes: 5 additions & 3 deletions packages/server/src/adapters/node/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
import type { HTTPPath } from '@orpc/contract'
import type { Promisable } from '@orpc/shared'
import type { IncomingMessage, ServerResponse } from 'node:http'
import type { Context } from '../../types'
import type { Context } from '../../context'

export type RequestHandleOptions<T extends Context> =
& { prefix?: HTTPPath, beforeSend?: (response: Response, context: T) => Promisable<void> }
& (undefined extends T ? { context?: T } : { context: T })
& (Record<never, never> extends T ? { context?: T } : { context: T })

export type RequestHandleRest<T extends Context> = [options: RequestHandleOptions<T>] | (undefined extends T ? [] : never)
export type RequestHandleRest<T extends Context> =
| [options: RequestHandleOptions<T>]
| (Record<never, never> extends T ? [] : never)

export type RequestHandleResult = { matched: true } | { matched: false }

Expand Down
40 changes: 20 additions & 20 deletions packages/server/src/builder-with-errors-middlewares.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { BuilderWithErrorsMiddlewares } from './builder-with-errors-middlewares'
import type { Context } from './context'
import type { ORPCErrorConstructorMap } from './error'
import type { Lazy } from './lazy'
import type { MiddlewareOutputFn } from './middleware'
Expand All @@ -8,7 +9,6 @@ import type { ProcedureBuilderWithInput } from './procedure-builder-with-input'
import type { ProcedureBuilderWithOutput } from './procedure-builder-with-output'
import type { DecoratedProcedure } from './procedure-decorated'
import type { AdaptedRouter, RouterBuilder } from './router-builder'
import type { WELL_CONTEXT } from './types'
import { z } from 'zod'

const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) })
Expand All @@ -26,11 +26,11 @@ const errors = {
},
}

const builder = {} as BuilderWithErrorsMiddlewares<{ db: string }, { auth?: boolean }, typeof baseErrors>
const builder = {} as BuilderWithErrorsMiddlewares<{ db: string }, { db: string, auth?: boolean }, typeof baseErrors>

describe('BuilderWithErrorsMiddlewares', () => {
it('.errors', () => {
expectTypeOf(builder.errors(errors)).toEqualTypeOf<BuilderWithErrorsMiddlewares<{ db: string }, { auth?: boolean }, typeof errors & typeof baseErrors>>()
expectTypeOf(builder.errors(errors)).toEqualTypeOf<BuilderWithErrorsMiddlewares<{ db: string }, { db: string, auth?: boolean }, typeof errors & typeof baseErrors>>()

// @ts-expect-error --- not allow redefine error map
builder.errors({ BASE: baseErrors.BASE })
Expand All @@ -39,7 +39,7 @@ describe('BuilderWithErrorsMiddlewares', () => {
it('.use', () => {
const applied = builder.use(({ context, next, path, procedure, errors }, input, output) => {
expectTypeOf(input).toEqualTypeOf<unknown>()
expectTypeOf(context).toEqualTypeOf<{ db: string } & { auth?: boolean }>()
expectTypeOf(context).toEqualTypeOf<{ db: string, auth?: boolean }>()
expectTypeOf(path).toEqualTypeOf<string[]>()
expectTypeOf(procedure).toEqualTypeOf<ANY_PROCEDURE>()
expectTypeOf(output).toEqualTypeOf<MiddlewareOutputFn<unknown>>()
Expand All @@ -52,7 +52,7 @@ describe('BuilderWithErrorsMiddlewares', () => {
})
})

expectTypeOf(applied).toEqualTypeOf < BuilderWithErrorsMiddlewares < { db: string }, { auth?: boolean } & { extra: boolean }, typeof baseErrors>>()
expectTypeOf(applied).toEqualTypeOf < BuilderWithErrorsMiddlewares < { db: string }, { db: string, auth?: boolean } & { extra: boolean }, typeof baseErrors>>()

// @ts-expect-error --- conflict context
builder.use(({ next }) => next({ db: 123 }))
Expand All @@ -64,26 +64,26 @@ describe('BuilderWithErrorsMiddlewares', () => {

it('.route', () => {
expectTypeOf(builder.route({ path: '/test', method: 'GET' })).toEqualTypeOf<
ProcedureBuilder<{ db: string }, { auth?: boolean }, typeof baseErrors>
ProcedureBuilder<{ db: string }, { db: string, auth?: boolean }, typeof baseErrors>
>()
})

it('.input', () => {
expectTypeOf(builder.input(schema)).toEqualTypeOf<
ProcedureBuilderWithInput<{ db: string }, { auth?: boolean }, typeof schema, typeof baseErrors>
ProcedureBuilderWithInput<{ db: string }, { db: string, auth?: boolean }, typeof schema, typeof baseErrors>
>()
})

it('.output', () => {
expectTypeOf(builder.output(schema)).toEqualTypeOf<
ProcedureBuilderWithOutput<{ db: string }, { auth?: boolean }, typeof schema, typeof baseErrors>
ProcedureBuilderWithOutput<{ db: string }, { db: string, auth?: boolean }, typeof schema, typeof baseErrors>
>()
})

it('.handler', () => {
const procedure = builder.handler(({ input, context, procedure, path, signal, errors }) => {
expectTypeOf(input).toEqualTypeOf<unknown>()
expectTypeOf(context).toEqualTypeOf<{ db: string } & { auth?: boolean }>()
expectTypeOf(context).toEqualTypeOf<{ db: string, auth?: boolean }>()
expectTypeOf(procedure).toEqualTypeOf<ANY_PROCEDURE>()
expectTypeOf(path).toEqualTypeOf<string[]>()
expectTypeOf(signal).toEqualTypeOf<undefined | InstanceType<typeof AbortSignal>>()
Expand All @@ -93,26 +93,26 @@ describe('BuilderWithErrorsMiddlewares', () => {
})

expectTypeOf(procedure).toMatchTypeOf<
DecoratedProcedure<{ db: string }, { auth?: boolean }, undefined, undefined, number, typeof baseErrors>
DecoratedProcedure<{ db: string }, { db: string, auth?: boolean }, undefined, undefined, number, typeof baseErrors>
>()
})

it('.prefix', () => {
expectTypeOf(builder.prefix('/test')).toEqualTypeOf<
RouterBuilder<{ db: string }, { auth?: boolean }, typeof baseErrors>
RouterBuilder<{ db: string }, { db: string, auth?: boolean }, typeof baseErrors>
>()
})

it('.tag', () => {
expectTypeOf(builder.tag('test', 'test2')).toEqualTypeOf<
RouterBuilder<{ db: string }, { auth?: boolean }, typeof baseErrors>
RouterBuilder<{ db: string }, { db: string, auth?: boolean }, typeof baseErrors>
>()
})

it('.router', () => {
const router = {
ping: {} as Procedure<{ db: string }, { auth?: boolean }, undefined, undefined, unknown, typeof errors>,
pong: {} as Procedure<WELL_CONTEXT, undefined, undefined, undefined, unknown, Record<never, never>>,
ping: {} as Procedure<{ db: string }, { db: string, auth?: boolean }, undefined, undefined, unknown, typeof errors>,
pong: {} as Procedure<Context, Context, undefined, undefined, unknown, Record<never, never>>,
}

expectTypeOf(builder.router(router)).toEqualTypeOf<
Expand All @@ -121,7 +121,7 @@ describe('BuilderWithErrorsMiddlewares', () => {

builder.router({
// @ts-expect-error - context is not match
ping: {} as Procedure<{ auth: 'invalid' }, undefined, undefined, undefined, unknown, typeof errors>,
ping: {} as Procedure<{ auth: 'invalid' }, Context, undefined, undefined, unknown, typeof errors>,
})

const invalidErrorMap = {
Expand All @@ -133,14 +133,14 @@ describe('BuilderWithErrorsMiddlewares', () => {

builder.router({
// @ts-expect-error - error map is not match
ping: {} as ContractProcedure<undefined, typeof schema, typeof invalidErrorMap>,
ping: {} as Procedure<Context, Context, undefined, undefined, unknown, typeof invalidErrorMap>,
})
})

it('.lazy', () => {
const router = {
ping: {} as Procedure<{ db: string }, { auth?: boolean }, undefined, undefined, unknown, typeof errors>,
pong: {} as Procedure<WELL_CONTEXT, undefined, undefined, undefined, unknown, Record<never, never>>,
ping: {} as Procedure<{ db: string }, { db: string, auth?: boolean }, undefined, undefined, unknown, typeof errors>,
pong: {} as Procedure<Context, Context, undefined, undefined, unknown, Record<never, never>>,
}

expectTypeOf(builder.lazy(() => Promise.resolve({ default: router }))).toEqualTypeOf<
Expand All @@ -149,13 +149,13 @@ describe('BuilderWithErrorsMiddlewares', () => {

// @ts-expect-error - context is not match
builder.lazy(() => Promise.resolve({ default: {
ping: {} as Procedure<{ auth: 'invalid' }, undefined, undefined, undefined, unknown, typeof errors>,
ping: {} as Procedure<{ auth: 'invalid' }, Context, undefined, undefined, unknown, typeof errors>,
} }))

// @ts-expect-error - error map is not match
builder.lazy(() => Promise.resolve({
default: {
ping: {} as Procedure<WELL_CONTEXT, undefined, undefined, undefined, unknown, { BASE: { message: 'invalid' } }>,
ping: {} as Procedure<Context, Context, undefined, undefined, unknown, { BASE: { message: 'invalid' } }>,
},
}))
})
Expand Down
Loading
Loading